5.1 实现 Element 更新的主流程

Element 更新的主流程实现, 之前在实现 reactivity 响应式时, 使用了 effect 监视器,用于监视响应式数据的变化, 进行收集依赖 & 触发依赖 的动作。
在实现 Element更新时,在setup 函数中使用了 响应式数据,在初始化 Element中的 patch() 时, 使用effct进行监控 虚拟节点的变化 。

happy path

导出响应式函数

  1. /* reactivity/index.ts */
  2. export { ref } from "./ref"

实现在 render 模板中 ref 定义的数据,不使用 .value 形式访问 , 在 setup() 的返回值中用 proxyRefs()包裹

  1. /* component.ts*/
  2. function handlerSetupResult(instance, setupResult) {
  3. if (isObject(setupResult)) {
  4. // 将 setup 的结果 注入 instance
  5. // 使用 proxyRefs 函数包裹,可以 让ref 数据在 render() 中不再使用 .value 方式访问数据
  6. instance.setupState = proxyRefs(setupResult)
  7. }
  8. }
  1. /* update/App.js */
  2. // 实现组件的更新流程
  3. import { h, ref } from "../../lib/guide-mini-vue.esm.js"
  4. export const App = {
  5. name: "App",
  6. setup() {
  7. const count = ref(0)
  8. const onClick = () => {
  9. // 响应式数据发生变化
  10. count.value++
  11. }
  12. return { count, onClick }
  13. },
  14. render() {
  15. return h(
  16. "div",
  17. {},
  18. [
  19. // 这里不再使用 this.count.value
  20. h("div", {}, `count: ${this.count}`),
  21. h("button", { onClick: this.onClick}, "Click me")
  22. ]
  23. )
  24. },
  25. }

初始时, 模板展示
image.png
使用 effect 监视器,监视 element 模板内容

定义初始化数据

  1. /* component.ts */
  2. export function createComponentInstance(vnode, parent) {
  3. const component = {
  4. // isMounted, 使用 isMounted 判断是否 init 初始化
  5. // 初始为 false
  6. isMounted: false,
  7. // 使用 instance.subTree 存储 subTree, 在更新时候需要用, 需要在 component 初始化 subTree
  8. // 定义 subTree 存储上一次更新的 VNode
  9. subTree: {},
  10. }
  11. }

使用 effect 监视 element变化

  1. /* renderer.ts */
  2. function setupRenderEffect(instance, initialVNode, container) {
  3. // 使用 effect监视模板中响应式数据的变化
  4. effect(() => {
  5. // 然后实现依赖收集 & 触发依赖的实现
  6. // 把 渲染的逻辑 写在这里, 收集依赖
  7. // 使用 isMounted 判断是否 init 初始化
  8. if (!instance.isMounted) {
  9. // 初始化
  10. console.log("init")
  11. const { proxy } = instance
  12. // call render()
  13. // 使用 instance.subTree 存储 subTree, 在更新时候需要用, 需要在 component 初始化 subTree
  14. const subTree = (instance.subTree = instance.render.call(proxy)) // 将proxy 注入到 render() 中
  15. // 初始化 传入 n1, 初始化为 null -> 没有老的虚拟节点
  16. patch(null, subTree, container, instance)
  17. initialVNode.el = subTree.el
  18. // 改变 isMounted 状态
  19. instance.isMounted = true
  20. } else {
  21. // 更新逻辑
  22. console.log("update")
  23. // 实现: 在初始化时 instance 中用一个变量 subTree , 保存当前实例的 subTree 的值
  24. // 在更新时候,获取到 上一个 subTree
  25. const { proxy } = instance
  26. // 拿到更新后的 subTree
  27. const subTree = instance.render.call(proxy)
  28. // 取出 之前保存上一次组件的 subTree
  29. const prevSubTree = instance.subTree
  30. // 实现 patch() 的更新逻辑
  31. // 添加 n1 n2; 老的虚拟节点 & 新的虚拟节点 -- 在这里进行赋值
  32. patch(prevSubTree, subTree, container, instance)
  33. }
  34. })
  35. }

处理初始化 patch() 传入的 老虚拟节点 & 新的虚拟节点 ; n1, n2

  1. /*renderer.ts*/
  2. function render(vnode, container) {
  3. // 调用 path
  4. // 初始化逻辑 n1 -> null
  5. patch(null, vnode, container, null)
  6. }
  7. // patch() 接收新的参数
  8. // n1 是旧的虚拟节点
  9. // n2 是新的虚拟节点
  10. // 如果 n1 不存在 --- 初始化 , n1 存在那就是 更新逻辑
  11. function patch(n1, n2, container, parentComponent) {
  12. const { type, shapeFlag } = n2 // type 组件的类型
  13. switch (type) {
  14. case Fragment:
  15. processFragment(n1, n2, container, parentComponent)
  16. case Text:
  17. processText(n1, n2, container)
  18. default:
  19. if (shapeFlag & ShapeFlags.ELEMENT) {
  20. processElement(n1, n2, container, parentComponent)
  21. } else if (shapeFlag & ShapeFlags.STATIC_COMPONENT) {
  22. processComponent(n1, n2, container, parentComponent)
  23. }
  24. }
  25. }
  26. // 用于处理 Text 文本节点
  27. function processText(n1, n2, container) {
  28. }
  29. // 处理Element
  30. function processElement(n1, n2, container, parentComponent) {
  31. // 判断 n1 是否存在
  32. if (!n1) {
  33. // 如果不存在 表示执行初始化逻辑
  34. // 初始化
  35. mountElement(n2, container, parentComponent)
  36. } else {
  37. // n1 具有数据 , 进行更新逻辑
  38. patchElement(n1, n2, container)
  39. }
  40. }
  41. // 实现 Element 更新的逻辑
  42. function patchElement(n1, n2, container) {
  43. console.log("patchElement")
  44. console.log("n1", n1)
  45. console.log("n2", n2)
  46. }
  47. // 修改
  48. function mountChildren(vnode, container, parentComponent) {
  49. vnode.children.forEach((v) => {
  50. // 组件逻辑, n1 为 null
  51. patch(null, v, container, parentComponent)
  52. })
  53. }

此时已经实现 Element 更新的主流程
image.png

5.2 实现 props 的更新

  1. 实现 Props 的更新
  2. props的更新要求
  3. 1. foo 之前的值 之后的值不一样了, 修改 props 属性
  4. 2. foo 更新后变为了 null | undefined 删除了 foo 属性
  5. 3. bar 更新后这个属性在更新后没有了, 删除 bar 属性

测试 props 的App 组件

  1. /* update/updateProps/App.js */
  2. export const App = {
  3. name: "App",
  4. setup() {
  5. // 定义响应式数据
  6. const count = ref(0)
  7. // 定义事件,修改响应式数据
  8. const onClick = () => {
  9. count.value++
  10. }
  11. const props = ref({
  12. foo: "foo",
  13. bar: "bar"
  14. })
  15. // 修改 props
  16. const onChangePropsDemo1 = () => {
  17. props.value.foo = "new-foo"
  18. }
  19. // 修改 bar
  20. const onChangePropsDemo2 = () => {
  21. props.value.foo = undefined
  22. }
  23. // 删除 bar 属性
  24. const onChangePropsDemo3 = () => {
  25. props.value = {
  26. foo: "foo"
  27. }
  28. }
  29. return {
  30. count,
  31. onClick,
  32. props,
  33. onChangePropsDemo1,
  34. onChangePropsDemo2,
  35. onChangePropsDemo3,
  36. }
  37. },
  38. // 渲染函数
  39. render() {
  40. // console.log(this.count)
  41. return h(
  42. "div",
  43. {
  44. id: "root",
  45. // 设置props
  46. ...this.props
  47. },
  48. [
  49. h('div', {}, `count: ${this.count}`),
  50. h('button',
  51. {
  52. // 点击事件
  53. onClick: this.onClick
  54. },
  55. 'click me'
  56. ),
  57. h(
  58. 'button',
  59. { onClick: this.onChangePropsDemo1 },
  60. // 直接修改 props.foo 的值
  61. 'foo-值改变了-修改'
  62. ),
  63. h(
  64. 'button',
  65. { onClick: this.onChangePropsDemo2 },
  66. 'foo值变成了undefined - 删除'
  67. ),
  68. h(
  69. 'button',
  70. { onClick: this.onChangePropsDemo3 },
  71. 'bar - 删除'
  72. )
  73. ]
  74. )
  75. }
  76. }

实现 Props 更新的逻辑

  1. /* renderer.ts */
  2. // 实现 Element 更新的逻辑
  3. function patchElement(n1, n2, container) {
  4. console.log("patchElement")
  5. console.log("n1", n1)
  6. console.log("n2", n2)
  7. // 执行Props的更新
  8. // 取到 n1 和 n2 的 props , 如果 props 为空 , 赋值为 EMPTY_OBJ 空对象
  9. const oldProps = n1.props || EMPTY_OBJ
  10. const newProps = n2.props || EMPTY_OBJ
  11. // 取到 n1的el, 赋值给 n2.el
  12. const el = (n2.el = n1.el)
  13. // 定义 更新 patchProps 的函数
  14. patchProps(el, oldProps, newProps)
  15. }
  16. // 实现 patchProps 更新Props的函数
  17. function patchProps(el, oldProps, newProps) {
  18. /**
  19. * 实现 Props 的更新
  20. *
  21. * props的更新要求
  22. * 1. foo 之前的值 与 之后的值不一样了, 修改 props 属性
  23. * 2. foo 更新后变为了 null | undefined ; 删除了 foo 属性
  24. * 3. bar 更新后这个属性在更新后没有了, 删除 bar 属性
  25. */
  26. // 优化1. 当 oldProps 与 newProps 不相同时,需要更新
  27. if (oldProps !== newProps) {
  28. // 实现1. 循环 newProps 与 oldProps 做对比
  29. for (const key in newProps) {
  30. // 取到 oldProps 的 props
  31. const prevProp = oldProps[key]
  32. // 取到 newProps 的 props
  33. const nextProp = newProps[key]
  34. // 判断 二者props是否一样
  35. if (prevProp !== nextProp) {
  36. // 不等时, 进行更新逻辑
  37. // 调用 hostPatchProp() 接口 , 因为就是在 hostPatchProp 这里实现props的赋值的
  38. hostPatchProp(el, key, prevProp, nextProp)
  39. }
  40. }
  41. // 优化2: 如果 oldProps 为空,不用进行对比
  42. if (oldProps !== EMPTY_OBJ) {
  43. // 实现3. bar 更新后这个属性在更新后没有了, 删除 bar 属性
  44. // 遍历 oldProps
  45. for (const key in oldProps) {
  46. // 如果 key 不在 newProps 中
  47. if (!(key in newProps)) {
  48. // 删除这个 key
  49. // 调用 hostPatchProp() 接口
  50. hostPatchProp(el, key, oldProps[key], null)
  51. }
  52. }
  53. }
  54. }
  55. }

修改一些重构代码

  1. /* runtime-dom/index.ts */
  2. // 给Element 添加属性
  3. function patchProp(el, key, prevProp, nextVal) { // 添加 prevProps 属性
  4. const isOn = () => /^on[A-Z]/.test(key)
  5. if (isOn()) {
  6. // 这里实现注册事件
  7. el.addEventListener(key.slice(2).toLowerCase(), nextVal)
  8. } else {
  9. // 实现2: foo 更新后变为了 null | undefined ; 删除了 foo 属性
  10. // 新增判断: 如果 nextVal 为 undefined || null 时
  11. if (nextVal === undefined || nextVal === null) {
  12. // 执行删除
  13. el.removeAttribute(key)
  14. } else {
  15. // 赋值属性
  16. el.setAttribute(key, nextVal)
  17. }
  18. }
  19. }
  1. /* shared/index.ts */
  2. // 导出使用同一个 EMPTY_OBJ 空对象
  3. export const EMPTY_OBJ = {}

实现
image.png

5.3 实现 children 的更新

实现 Element 中的 chlidren 的更新逻辑
测试组件 , 都存入updateChilren 文件夹

Array -> Text

  1. /* App.js */
  2. // 实现 Element - Children 更新流程
  3. import { h } from "../../../lib/guide-mini-vue.esm.js"
  4. import ArrayToText from "./ArrayToText.js"
  5. import TextToText from "./TextToText.js"
  6. import TextToArray from "./TextToArray.js"
  7. import ArrayToArray from "./ArrayToArray.js"
  8. export const App = {
  9. name: "App",
  10. setup() {
  11. },
  12. render() {
  13. return h("div", { tTd: 1 },
  14. [
  15. h('p', {}, "主页"),
  16. // 1. 老的是 Array , 新的是Text
  17. h(ArrayToText),
  18. // 老的是 Text , 新的是 Text
  19. // h(TextToText),
  20. // 老的是 Text , 新的是 Array
  21. // h(TextToArray),
  22. // 老的是 Array , 新的是 Array
  23. // h(ArrayToArray)
  24. ])
  25. }
  26. }
  1. /* example/update/patchChildren/ArrayToText.js */
  2. // 新的节点 -> Text
  3. const nextChildren = "newChildren"
  4. // 老的节点 -> Array
  5. const prevChildren = [
  6. h('div', {}, "A"),
  7. h("div", {}, "B")
  8. ]
  9. export default {
  10. name: "ArrayToText",
  11. setup() {
  12. // 定义响应式数据
  13. const isChange = ref(false)
  14. // 全局变量
  15. window.isChange = isChange
  16. return {
  17. isChange
  18. }
  19. },
  20. render() {
  21. const self = this
  22. // 判断响应式数据变化
  23. return self.isChange === true
  24. ? h('div', {}, nextChildren) // 展示新的节点
  25. : h("div", {}, prevChildren) // 展示旧的节点
  26. },
  27. }

具体实现

  1. /**
  2. * 实现 Children 的更新逻辑
  3. *
  4. * 1. 先判断当前节点 和 老的节点 的 children 的状态,看看他是 text 还是 Array
  5. * 2. 实现更新内容, 基于 shapeFlags 判断 children 的类型,
  6. * 2.1 如果是 Array -> Text 节点
  7. * - 实现: 1. 先清空掉数组, 2. 设置文本
  8. * 2.2 如果是 Text -> Text
  9. * - 实现: 判断老的Text 和 新的 Text 是否相同,如果不同,则更新文本
  10. * 2.3 如果是 Text -> Array
  11. * - 实现:1. 把Text清空,让后获取到Array的children , 重新进行mountChildren 渲染
  12. */
  1. /* renderer.ts */
  2. // 实现 Element 更新的逻辑
  3. function patchElement(n1, n2, container) {
  4. /* 其他代码 */
  5. // 定义 更新 patchChildren 的函数
  6. patchChildren(n1, n2, el)
  7. }
  8. // 实现 Children 的更新
  9. function patchChildren(n1, n2, el) { // 传入 el -> 当前标签的父元素 父标签
  10. // 1. 获取 n1 和 n2 的 children 的状态
  11. // 新节点的状态 是 Text 还是 Array
  12. const { shapeFlag } = n2
  13. // 老节点的状态 是 Text 还是 Array
  14. const prevShapeFlag = n1.shapeFlag
  15. // 获取新老节点的 children
  16. const c1 = n1.children
  17. const c2 = n2.children
  18. // 2. 判断 n2 的 shapeFlag 新节点的 ShapeFlag
  19. if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  20. // 如果新的节点 是 Text 节点
  21. // 如果老的节点 是 Array 节点
  22. if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  23. // Array -> Text
  24. // 清空老节点的 Array
  25. unmountChildren(c1)
  26. // 设置新节点的文本
  27. // 传入 div -> 也就是 老的节点的 el , 和新的节点的 children
  28. // 写在runtime-dom 的接口上 -> 与 Element 渲染一致
  29. hostSetElementText(el, c2)
  30. }
  31. } else {
  32. // 新的节点 是 Array 节点, 因为只有这两种状态
  33. }
  34. }
  35. // 清空 children -> Array
  36. function unmountChildren(children) {
  37. // 遍历 children
  38. for (let i = 0; i < children.length; i++) {
  39. // 获取每一个 children 的 el
  40. // 也就是它的父标签 div
  41. const child = children[i].el
  42. // 执行删除的逻辑
  43. // 写在runtime-dom 的接口上 -> 与 Element 渲染一致
  44. // 传入 获取到的 el 标签
  45. hostRemove(child)
  46. }
  47. }

runtime-dom实现删除Array和 添加 Text的接口方法

  1. /* runtime-dom/index.ts */
  2. // 执行删除 children 的逻辑
  3. function remove(el) {
  4. // 1. 拿到 child 父级的 节点 -> div = el.parentNode
  5. const parent = el.parentNode
  6. // 判断是否存在
  7. if (parent) {
  8. // 执行删除, 使用el的父节点删除 el
  9. parent.removeChild(el)
  10. }
  11. }
  12. // SetElementText
  13. // 实现添加文本节点的接口
  14. function setElementText(el, text) {
  15. el.textContent = text
  16. }
  17. const renderer: any = createRender({
  18. createElement,
  19. patchProp,
  20. insert,
  21. // 导出
  22. remove,
  23. setElementText
  24. })

使用接口方法

  1. /* render.ts */
  2. const {
  3. createElement: hostCreateElement,
  4. patchProp: hostPatchProp,
  5. insert: hostInsert,
  6. remove: hostRemove,
  7. setElementText: hostSetElementText
  8. } = options

页面修改
image.png

Text -> Text

  1. /* TextToText.js */
  2. // 新的是 text
  3. // 老的是 text
  4. const prevChildren = "oldChild";
  5. const nextChildren = "newChild";
  6. export default {
  7. name: "TextToText",
  8. setup() {
  9. /* 其他代码 */
  10. },
  11. render() {
  12. const self = this;
  13. return self.isChange === true
  14. ? h("div", {}, nextChildren)
  15. : h("div", {}, prevChildren);
  16. },
  17. };

实现 Text -> Text 的逻辑

  1. /* renderer.ts */
  2. // 实现 Children 的更新
  3. function patchChildren(n1, n2, el) { // 传入 el -> 当前标签的父元素 父标签
  4. // 1. 获取 n1 和 n2 的 children 的状态
  5. // 新节点的状态 是 Text 还是 Array
  6. const { shapeFlag } = n2
  7. // 老节点的状态 是 Text 还是 Array
  8. const prevShapeFlag = n1.shapeFlag
  9. // 获取新老节点的 children
  10. const c1 = n1.children
  11. const c2 = n2.children
  12. // 2. 判断n2 的 shapeFlag
  13. if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  14. // 如果新的节点 是 Text 节点
  15. // 如果老的节点 是 Array 节点
  16. if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  17. // 清空老节点的 Array
  18. unmountChildren(c1)
  19. // 设置新节点的文本
  20. // 传入 div -> 也就是 老的节点的 el , 和新的节点的 children
  21. // 写在runtime-dom 的接口上 -> 与 Element 渲染一致
  22. hostSetElementText(el, c2)
  23. } else {
  24. // 如果老的节点 是 Text 节点
  25. // Text -> Text
  26. // 当两个节点不相等时才取更新
  27. if (c1 !== c2) {
  28. // 更新文本
  29. hostSetElementText(el, c2)
  30. }
  31. }
  32. } else {
  33. // 新的节点 是 Array 节点, 因为只有这两种状态
  34. }
  35. }

image.png
实现设置文本代码重构

  1. /* renderer.ts */
  2. // 实现 Children 的更新
  3. function patchChildren(n1, n2, el) {
  4. const { shapeFlag } = n2
  5. const prevShapeFlag = n1.shapeFlag
  6. const c1 = n1.children
  7. const c2 = n2.children
  8. if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  9. // 如果老的节点 是 Array 节点
  10. if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  11. // 清空老节点的 Array
  12. unmountChildren(c1)
  13. }
  14. // 重构 因为以上代码使用了两次 hostSetElementText
  15. // 设置新节点的文本
  16. // 传入 div -> 也就是 老的节点的 el , 和新的节点的 children
  17. // 写在runtime-dom 的接口上 -> 与 Element 渲染一致
  18. // hostSetElementText(el, c2)
  19. // 2. 如果老的节点 是 Text 节点
  20. // 当两个节点不相等时才取更新
  21. if (c1 !== c2) {
  22. // 更新文本
  23. hostSetElementText(el, c2)
  24. }
  25. } else {
  26. // 新的节点 是 Array 节点, 因为只有这两种状态
  27. }
  28. }

Text -> Array

  1. /* TextToArray.js */
  2. import { h, ref } from "../../../lib/guide-mini-vue-esm.js"
  3. // 新的是 array
  4. // 老的是 text
  5. const prevChildren = "oldChild";
  6. const nextChildren = [h("div", {}, "A"), h("div", {}, "B")];
  7. export default {
  8. name: "TextToArray",
  9. setup() {
  10. // 气态代码
  11. },
  12. render() {
  13. const self = this;
  14. return self.isChange === true
  15. ? h("div", {}, nextChildren)
  16. : h("div", {}, prevChildren);
  17. },
  18. };

实现 Text -> Array 的逻辑

  1. /* renderer.ts */
  2. // 这里往上层传入 parentComponent , 因为mountChildren 需要
  3. function patchChildren(n1, n2, container, parentComponent) {
  4. const { shapeFlag } = n2
  5. // 老节点的状态 是 Text 还是 Array
  6. const prevShapeFlag = n1.shapeFlag
  7. // 获取新老节点的 children
  8. const c1 = n1.children
  9. const c2 = n2.children
  10. // 2. 判断n2 的 shapeFlag
  11. if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  12. // 如果新的节点 是 Text 节点
  13. // 如果老的节点 是 Array 节点
  14. if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  15. // 清空老节点的 Array
  16. unmountChildren(c1)
  17. }
  18. if (c1 !== c2) {
  19. // 更新文本
  20. hostSetElementText(container, c2)
  21. }
  22. } else {
  23. // 新的节点 是 Array 节点, 因为只有这两种状态
  24. // 判断 n1 的 shapeFlag, 判断之前的是不是Text节点
  25. if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
  26. // Text -> Array
  27. // 1. 清空 Text
  28. hostSetElementText(container, "")
  29. // 2. 渲染 Array
  30. mountChildren(c2, container, parentComponent)
  31. } else {
  32. // Array -> Array
  33. }
  34. }
  35. }
  1. /* renderer.ts */
  2. // 初始化渲染时 - 修改 vnode -> vnode.children
  3. // 3. children
  4. const { children, shapeFlag } = vnode
  5. if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  6. // if (typeof children === 'string') {
  7. el.textContent = children
  8. } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  9. // 改为传入 VNode 的 children
  10. mountChildren(vnode.children, el, parentComponent)
  11. }
  12. // 修改一些代码
  13. function mountChildren(children, container, parentComponent) {
  14. // 修改为 children
  15. children.forEach((v) => {
  16. // 组件逻辑, n1 为 null
  17. patch(null, v, container, parentComponent)
  18. })
  19. }

image.png

Array -> Array

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