vue3渲染流程

测试用例

  1. it('should allow attrs to fallthrough', async () => {
  2. debugger
  3. const click = jest.fn()
  4. const childUpdated = jest.fn()
  5. const Hello = {
  6. setup() { // 如果有render 就优先渲染render了
  7. const count = ref(0)
  8. function inc() { // 需要了解onClick事件系统
  9. count.value++
  10. click()
  11. }
  12. // 这里的任何东西 都不会被本身effect收集 只有return 后的方法 才会
  13. return () => // 这里还可以当redner来使用 我平时会的只是 return {} template所需要的事件或属性 所以是检查function 还是 object来判断的吧
  14. h(Child, { // 那是不是有一个props effect? 标记也要注意下
  15. foo: count.value + 1,
  16. id: 'test',
  17. class: 'c' + count.value,
  18. style: { color: count.value ? 'red' : 'green' },
  19. onClick: inc,
  20. 'data-id': count.value + 1
  21. })
  22. },
  23. mounted() {
  24. console.log('?')
  25. }
  26. }
  27. const Child = { // 原来是这样传参数的 那么就不需要this 什么的了
  28. setup(props: any) {
  29. onUpdated(childUpdated)
  30. return () =>
  31. h(
  32. 'div',
  33. {
  34. class: 'c2',
  35. style: { fontWeight: 'bold' }
  36. },
  37. props.foo // 这个为什么是undefinded呢 因为setFullProps执行的时候,判断到的赋值Props方式为attrs,所以instance.props为空
  38. )
  39. }
  40. }
  41. const root = document.createElement('div')
  42. document.body.appendChild(root)
  43. render(h(Hello), root) // 这个render 是'@vue/runtime-dom' 我们之前用的 是'@vue/runtime-test' 里面的 测试用的... 但是区别不一样的就是 会不会初始化而已
  44. const node = root.children[0] as HTMLElement
  45. expect(node.getAttribute('id')).toBe('test')
  46. expect(node.getAttribute('foo')).toBe('1')
  47. expect(node.getAttribute('class')).toBe('c2 c0')
  48. expect(node.style.color).toBe('green')
  49. expect(node.style.fontWeight).toBe('bold')
  50. expect(node.dataset.id).toBe('1')
  51. node.dispatchEvent(new CustomEvent('click')) // 事件触发a
  52. node.dispatchEvent(new CustomEvent('click')) // 事件触发a
  53. expect(click).toHaveBeenCalled()
  54. await nextTick()
  55. expect(childUpdated).toHaveBeenCalled()
  56. expect(node.getAttribute('id')).toBe('test')
  57. expect(node.getAttribute('foo')).toBe('2')
  58. expect(node.getAttribute('class')).toBe('c2 c1')
  59. expect(node.style.color).toBe('red')
  60. expect(node.style.fontWeight).toBe('bold')
  61. expect(node.dataset.id).toBe('2')
  62. })

请使用该测试用例进行单步调试。processComponent流程图

什么是vnode?网上搜索到的都是说visual dom用来描述真实的DOM标签,作用是可以通过render渲染成DOM,然后挂载。
这些对于入门的人,肯定一脸懵逼,其实vnode就是一个object记录了渲染过程所需要用到的数据而已,对于每一个字段的作用,深入下去,一个一个慢慢认识。
这里附带上vnode字段图

什么是instance?
和vnode一样的行为,但是这只应用于Component类型的组件,所以和vnode分开,作为一个vnode的拓展,当然vnode.component字段就包含对应的instance。
instance

什么是h?渲染函数&JSX
你为什么要了解这个?比如你的template是

  1. <template>
  2. <div id="1">123</div>
  3. </template>

会被compiler转换成h(‘div’, { id: 1 }, 123),这里不去会去说compiler的转换,单纯的讲render流程,
compiler做的东西还有静态标记之类的,这里只是举个简单的例子,将会在compiler章节详细说。

渲染过程

渲染流程 - 图1前排提示多喝热水,请打开渲染流程图instancevnode和单步调试来食用,
如果你没有使用电脑,无法进行单步调试,没关系,看着我的图也不是不行,我会以文字和概念的方式,尽量和你科普明白。这个颜色是运行的代码。

入口render(①vnode,②HtmlElement),①参数是vnode,②参数是你实际浏览器中的DOM,这一整个东西就是把vnode,变成一个真的DOM,然后插入到②参数DOM中。
我们先来了解一下这个①,这个①在当前测试用例中使用h(Hello)制作出来的,这做了啥?设置vnode.type = Hello,设置vnode.shapeFlag,
检测到传入的参数Hello是Object类型的,所以vnode.shapeFlag = ShapeFlags.STATEFUL_COMPONENT,顺便说一下normalizeChildren,如果有chlidren的情况下是用来加密vnode.shapeFlag的
,这里有没有chlidren的传入,可以忽略,啥?啥又是chlidren的传入?

  1. const testVnode = h('span', { id: 1 }, 'nihao')
  2. render(testVnode, HTMLDivElement)
  3. // 最终生成<div><span id="1">nihao</span></div>

这下了解了吧,这个nihao就是chldren。

渲染流程 - 图2什么,还不懂?那从头看一遍,懂了的可以继续往下看render到底做了啥。


把②参数命名为container,传入render的①vnode称为n2,container.vnode称为n1。

判断到n2不为空,进行patch,判断到n1 == null,且n2.shapeFlag进行decode,为ShapeFlags.COMPONENT类型,执行processComponent。
这里的decode和encode是什么来的?
我们在生成vnode的时候,vnode.type数据被normalizeChildren加密过,因为当前children为null,所以type为0,加密方式为vnode.shapeFlag |= type
解密方式为const a = vnode.shapeFlag & ShapeFlags.COMPONENT,只要a > 0就为true,ShapeFlags.COMPONENT为一个常数。

  1. export declare const enum ShapeFlags {
  2. ELEMENT = 1, // 1
  3. FUNCTIONAL_COMPONENT = 2, // 10
  4. STATEFUL_COMPONENT = 4, // 100
  5. TEXT_CHILDREN = 8, // 1000
  6. ARRAY_CHILDREN = 16, // 10000
  7. SLOTS_CHILDREN = 32, // 100000
  8. TELEPORT = 64, // 1000000
  9. SUSPENSE = 128, // 10000000
  10. COMPONENT_SHOULD_KEEP_ALIVE = 256, // 100000000
  11. COMPONENT_KEPT_ALIVE = 512, // 1000000000
  12. COMPONENT = 6 // 110
  13. }

渲染流程 - 图3想了解这套运行机制麽?上面被normalizeChildren过的,
是按位与的意思,比如二进制中

  1. 01 | 01 === 01
  2. 01 | 10 === 11
  3. 10 | 10 === 10

对应位置0 | 1 === 1, 0 | 0 === 0,就像||的逻辑,只要满足其中一个为真值。

判断类型的按位且

  1. 11 & 11 === 11
  2. 01 & 10 === 00
  3. 10 & 10 === 10

对应位置1 & 1 === 1, 1 | 0 === 0,就像&&的逻辑,要满足所有为真值。

有没有注意到上面typescript的ShapeFlags的枚举,后面有二进制注释,有没有注意到判断类型渲染,都是使用if来判断的,就是说只要大于0就行。
有没有发现了什么?再提醒一点,二进制标记法?没错normalizeChildren中,
只不过是两个类型利用二进制标记的合并,比如100代表STATEFUL_COMPONENT类型,10000代表ARRAY_CHILDREN类型,两个使用100 | 10000是不是等于10100,而在
patch中if(vnode.shapeFlag & ShapeFlags.COMPONENT),
ShapeFlags.COMPONENT为110,再看看那个表,是不是FUNCTIONAL_COMPONENT | STATEFUL_COMPONENT可以得到110?
所以10100 & 110为true,以上方法就是通过|来进行两种类型二进制占位符来合并,通过&判断该位置的值是否存在。

渲染流程 - 图4,是不是顿时大悟,感觉自己的代码质量大升,下次写代码也可以通过二进制标记来进行类型判断了!学废了没?