Element& Component都具有更新的逻辑 , 只不过 Component的更新没有太复杂

happy

创建需要测试通过的文件
App 组件

  1. /* example/componentUpdate/App.js */
  2. import Child from "./Child.js"
  3. // 实现组件更新的逻辑
  4. export const App = {
  5. name: "App",
  6. setup() {
  7. const msg = ref("123")
  8. const count = ref(1)
  9. window.msg = msg
  10. const changeChildrenProps = () => {
  11. msg.value = "456"
  12. }
  13. const changCount = () => {
  14. count.value++
  15. }
  16. return {
  17. msg, changeChildrenProps, count, changCount
  18. }
  19. },
  20. render() {
  21. return h(
  22. "div",
  23. {},
  24. [
  25. h('div', {}, "你好"),
  26. h('button', { onClick: this.changeChildrenProps }, "change children props"),
  27. // 传给 Child 组件的 props
  28. h(Child, { msg: this.msg }),
  29. // 修改 count
  30. h('button', { onClick: this.changCount }, "change self count"),
  31. h('p', {}, "count: " + this.count)
  32. ]
  33. )
  34. }
  35. }

Child 组件

  1. /* example/componentUpdate/Child.js */
  2. export default {
  3. name: "Child",
  4. setup(props, { emit }) {},
  5. render(proxy) {
  6. return h('div', {}, [
  7. // 在render 中,实现通过 this.$props 访问当前组件的 props
  8. h('div', {}, "child - props - msg: " + this.$props.msg)
  9. ])
  10. }
  11. }

实现 $props的逻辑
在访问render() 中, 能够通过 this 访问的的 属性, 都是通过 componentPublicInstance.ts添加属性 ,所以 $props 同样实现

  1. /* src/runtime-core/componentPublicInstance.ts */
  2. // 声明一个对象,根据key,判断是否有属性,返回 instance 上的值
  3. const publicPropertiesMap = {
  4. $el: (instance) => instance.vnode.el,
  5. $slots: (instance) => instance.slots
  6. // 通过 $props 访问到 当前组件的 props
  7. $props: (instance) => instance.props
  8. }

此时的页面
image.png

7.1 组件的更新逻辑

添加组件发生变化时 执行 函数

  1. /* renderer.ts*/
  2. // 处理组件
  3. function processComponent(n1, n2, container, parentComponent, anchor) {
  4. // 判断组件 是否为 更新 | 初始化
  5. if (!n1) {
  6. // 1. init
  7. mountComponent(n2, container, parentComponent, anchor)
  8. } else {
  9. // 2. update
  10. updateComponet(n1, n2)
  11. }
  12. }

实现组件更新的逻辑

  1. /**
  2. * 更新组件的逻辑: 重新调用当前组件的 render函数, 重新生成虚拟节点,再进行patch
  3. * -> 更新组件的props , 调用组件的render()
  4. *
  5. * 1. 当调用 响应式数据时,会重新执行 effect 函数,而 effect 函数会具有一个返回值
  6. * 2. effect 函数的返回值,可以再次effect 收集的依赖,进行调用 -> 赋值为 instance.update
  7. * 3. 在更新组件时, 调用 instance.update 就行
  8. * 4. 进行组件内容的更新 props
  9. * - 实现逻辑: 在更新逻辑需要更新之后的虚拟节点,也就是 n2, 使用 next 保存 n2 的虚拟节点
  10. * - 1. 赋值 instance.next , 并在 component 中初始化 next
  11. * - 2. 在更新的逻辑中 取出 next 更新的虚拟节点,和 vnode 当前的组件虚拟节点
  12. * - 3. 把el 进行更新 next.el = n1.el
  13. * - 4. 更新props
  14. * - 更新虚拟节点 : 老的虚拟节点 = 更新后的虚拟节点 next
  15. * - 清空 instance.next
  16. * - 更新 props : instance.props = next.props
  17. *
  18. * 5. 优化: 判断组件是否更新
  19. * - 实现判断是否更新的方法,主要就是判断 props 是否相等
  20. * - 1. 封装 shouldUpdateComponent() 函数 传入 n1 n2
  21. * - 2. 取出 n1 n2 的 props , 然后 循环对比 key 是否相同
  22. */
  1. /* component.ts */
  2. // 初始化 component
  3. const component = {
  4. // 初始化 update, 用于挂载 effect返回的 runner 函数
  5. update: null,
  6. // 定义 component 挂载当前组件的实例
  7. component: null,
  8. // 初始化 next - 用于保存更新后虚拟节点
  9. next: null
  10. }
  1. /* renderre.ts */
  2. // 更新组件函数
  3. function updateComponet(n1, n2) {
  4. // 1. 获取到 instance
  5. const instance = (n2.component = n1.component)
  6. // 优化 -> 判断组件是否更新
  7. if (shouldUpdateComponent(n1, n2)) {
  8. // 更新逻辑先执行这里
  9. // 2. 赋值 instance.next = n2
  10. // 更新之后的虚拟节点 为了在 effect 中拿到更新后的虚拟节点
  11. instance.next = n2
  12. // 当更新时候调用 instance.update -> 再次执行 effect 函数
  13. instance.update()
  14. } else {
  15. // 不需要更新时
  16. n2.el = n1.el
  17. instance.vnode = n2
  18. }
  19. }
  20. // 初始化组件
  21. function mountComponent(initialVNode, container, parentComponent, anchor) {
  22. // 获取 组件的初始化实例
  23. // 在初始化 组件时候 赋值 componet -> 当前组件的实例 instance
  24. const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent))
  25. }
  26. // effect 函数中执行更新组件
  27. // 拿到 effect 的返回值 runner函数, 并且把 runner 函数挂载在 instance.update 方法中
  28. // 需要到 componet 中初始化 update 方法
  29. instance.update = effect(() => {
  30. // 其他代码 ...
  31. } else {
  32. // 更新逻辑
  33. console.log("update")
  34. // 需要一个 更新之后的 虚拟节点 vnode -> instance.next 在之前有赋
  35. // 实现组件的更新
  36. // 取出 next & vnode 新的虚拟节点 & 老的虚拟节点
  37. const { next, vnode } = instance
  38. // 如果 next 虚拟节点有值
  39. if (next) {
  40. // 更新 el
  41. next.el = vnode.el
  42. // 更新组件属性 props
  43. updateComponetPreRender(instance, next)
  44. }
  45. // 其他 代码 ...
  46. }
  47. })
  48. // 组件的props更新逻辑
  49. function updateComponetPreRender(instance, nextVNode) {
  50. // 需要更新当前虚拟节点 -> 为 nextVNode
  51. instance.vnode = nextVNode
  52. // 最后清空 instance.next
  53. instance.next = null
  54. // 更新 props
  55. instance.props = nextVNode.props
  56. }

实现优化的更新的函数

  1. /* componentUpdateUtils.ts */
  2. // 判断组件是否更新的逻辑
  3. export function shouldUpdateComponent(prevVNode, nextVNode) {
  4. // 1. 获取props
  5. const { props: prevProps } = prevVNode
  6. const { props: nextProps } = nextVNode
  7. // 2. 循环对比
  8. for (const key in nextProps) {
  9. // 如果它们的 props key 不相等时,返回 true
  10. if (nextProps[key] !== prevProps[key]) {
  11. return true
  12. }
  13. }
  14. return false
  15. }

实现效果 :
image.png

7.2 实现 nextTick 逻辑

Vue3 视图更新的逻辑

vue3 视图更新实现逻辑
测试组件

  1. /* nextTicker/App.js */
  2. import { h, ref, getCurrentInstance } from "../../lib/guide-mini-vue-esm.js"
  3. export const App = {
  4. name: "App",
  5. setup() {
  6. const count = ref(1)
  7. // 当前组件的实例
  8. const instance = getCurrentInstance()
  9. // 点击循环 100 次 , 并更新视图
  10. const onClick = () => {
  11. for (let i = 0; i < 100; i++) {
  12. console.log("update")
  13. count.value = i
  14. }
  15. }
  16. // 获取当前组件实例
  17. console.log(instance)
  18. // 因为当执行 打印instance, 此时 count 已经为 99 了, 但是 instance 中页面上的 count 又是 1 , 这时进行同步的代码
  19. return {
  20. count,
  21. onClick
  22. }
  23. },
  24. render() {
  25. const button = h('button', { onClick: this.onClick }, "update")
  26. const p = h('p', {}, "count : " + this.count)
  27. return h("div", {}, [button, p])
  28. }
  29. }

初始时候,循环 100 次, 页面视图就会更新100 次 ;

解决 :
思路 : 使用微任务,当执行 for 循环时会执行 instance.update 100 次,我们只需呀最后渲染的一个,所以把 instance.update 存入一个队列中, 并且这个队列 不能有重复的 job ; 当 for 循环执行完, 微任务去这个队列中 取出 job 执行 ; 可以达到 instance.update 只更新一次

  • vue3 的视图更新是异步的

实现 :

  1. 使用 effect 实现的 schedule 第二个参数,实现微任务
  2. 定义函数可以把 instance.update 存储到 一个队列里
  3. 执行微任务 , 取出 job 然后执行

具体代码

  1. /* renderer.ts */
  2. // effect 函数中使用 schedule
  3. // 要把更新视图的逻辑添加到队列中, 所以更新逻辑不能立即执行
  4. instance.update = effect(() => {
  5. }, {
  6. // 使用 scheduler
  7. scheduler() {
  8. console.log("update - scheduler")
  9. // 调用 effect 返回出来的 fn -> instance.update
  10. // 所以这里 job 就是 instance.update, 当前组件发生变化的逻辑
  11. // 收集 job 的逻辑 queueJobs()
  12. queueJobs(instance.update)
  13. }
  14. })

创建 scheduler.ts 写微任务的逻辑

  1. /* scheduler.ts */
  2. // 1. 定义一个队列, 用于存储 job
  3. const queue: any[] = [];
  4. // 定义一个函数 queueJob , 收集 job 的方法
  5. // 所以这里 job 就是 instance.update, 当前组件发生变化的逻辑
  6. export function queueJobs(job) {
  7. // 2. 添加 job 到队列中
  8. // 判断当前的 job 在不在这个队列中
  9. if (!queue.includes(job)) { // 能达到 for 循环只能有个 job
  10. // 如果不在,才添加到队列中
  11. queue.push(job);
  12. }
  13. // 执行微任务
  14. queueFlush()
  15. }
  16. // 3. 在微任务时候执行 job
  17. function queueFlush() {
  18. Promise.resolve().then(() => {
  19. let job
  20. while ((job = queue.shift())) {
  21. // 取出job , 执行 job
  22. job && job()
  23. }
  24. })
  25. }

效果 : 页面内容只更新一次
image.png

优化代码

  1. // 1. 定义一个队列, 用于存储 job
  2. const queue: any[] = [];
  3. // 优化微任务执行 - 因为当前代码会执行 99 次 queueFlush
  4. // 定义一个 布尔值 -> false , 在执行queueFlush前判断一下
  5. let isFlushPending = false;
  6. // 定义一个函数 queueJob , 收集 job 的方法
  7. // 所以这里 job 就是 instance.update, 当前组件发生变化的逻辑
  8. export function queueJobs(job) {
  9. // 2. 添加 job 到队列中
  10. // 判断当前的 job 在不在这个队列中
  11. if (!queue.includes(job)) {
  12. // 如果不在,才添加到队列中
  13. queue.push(job);
  14. }
  15. // 因为当前代码会执行 99 次 queueFlush
  16. queueFlush()
  17. }
  18. function queueFlush() {
  19. // 如果 isFlushPending = false 执行代码
  20. if (isFlushPending) return;
  21. isFlushPending = true; // 关闭执行 微任务函数
  22. // 3. 在微任务时候执行 job
  23. Promise.resolve().then(() => {
  24. // 把 isFlushPending 打开
  25. isFlushPending = false;
  26. let job
  27. while ((job = queue.shift())) {
  28. // 取出job , 执行 job
  29. job && job()
  30. }
  31. })
  32. }

实现 nextTick

  1. setup() {
  2. const count = ref(1)
  3. // 当前组件的实例
  4. const instance = getCurrentInstance()
  5. // 点击循环 100 次 , 并更新视图
  6. const onClick = () => {
  7. for (let i = 0; i < 100; i++) {
  8. console.log("update")
  9. count.value = i
  10. }
  11. // 获取当前组件实例
  12. console.log(instance) // 因为执行到这里 还是主任务 (同步逻辑)
  13. // 因为当执行 打印instance, 此时 count 已经为 99 了, 但是 instance 中页面上的 count 又是 1 , 这时进行同步的代码
  14. // 所以使用 nextTick() 获取到更新后页面数据
  15. nextTick(() => { // 执行到这里就是 异步逻辑了
  16. // 拿到更细完成之后的 视图
  17. console.log(instance)
  18. })
  19. // 还可以使用
  20. // await nextTick()
  21. // console.log(instance)
  22. }
  23. },

nextTick 实现的逻辑
就是把接收的函数添加到微任务(异步队列中), 当执行完 视图更新 , nextTick 传入的异步函数就会执行

  1. /* scheduler.ts */
  2. // 实现 nextTick
  3. // 实现就是 把接收到的 fn ,添加到微任务就行
  4. export function nextTick(fn) {
  5. // 把fn添加到 微任务 , 异步执行 fn
  6. // 如果没有 fn , 也会执行 微任务 Promise.resolve(); -> 进行一个等待
  7. return fn ? Promise.resolve().then(fn) : Promise.resolve();
  8. // 还可以使用
  9. // await nextTick()
  10. // console.log(instance)
  11. }

重构代码

  1. /* scheduler.ts */
  2. // 实现 nextTick
  3. // 实现就是 把接收到的 fn ,添加到微任务就行
  4. export function nextTick(fn) {
  5. // 把fn添加到 微任务 , 异步执行 fn
  6. // 如果没有 fn , 也会执行 微任务 Promise.resolve(); -> 进行一个等待
  7. return fn ? Promise.resolve().then(fn) : Promise.resolve();
  8. // 还可以使用
  9. // await nextTick()
  10. // console.log(instance)
  11. }
  12. function queueFlush() {
  13. // 如果 isFlushPending = false 执行代码
  14. if (isFlushPending) return;
  15. isFlushPending = true; // 关闭执行 微任务函数
  16. // 3. 在微任务时候执行 job
  17. // 异步执行 job
  18. // Promise.resolve().then(() => {
  19. // // 把 isFlushPending 打开
  20. // isFlushPending = false;
  21. // let job
  22. // while ((job = queue.shift())) {
  23. // // 取出job , 执行 job
  24. // job && job()
  25. // }
  26. // })
  27. // 进行重构
  28. nextTick(flushJobs)
  29. }
  30. function flushJobs() {
  31. // 把 isFlushPending 打开
  32. isFlushPending = false;
  33. let job
  34. while ((job = queue.shift())) {
  35. // 取出job , 执行 job
  36. job && job()
  37. }
  38. }

实现效果

  • 此时主任务 视图还没有进行更细

image.png

使用 nextTick() 后 fn 转为异步的 , 又因为 vue3 的视图更细是异步的, 所以执行完 视图更新,就立即执行 nextTick(fn) ; 所以能够拿到页面更新后的效果
image.png

最后要知道的 vue3 视图更新为什么采用异步的方式 :

  • 如果是同步的 , 会出现 for 循环 100 次, 页面内容都要更细 100 次
  • 采用异步,的可以减少循环, 页面的渲染等等好处,基于 JS的异步任务队列 实现 nextTick