vue3渲染流程

测试用例

it('should allow attrs to fallthrough', async () => {
    debugger
    const click = jest.fn()
    const childUpdated = jest.fn()

    const Hello = {
      setup() { // 如果有render 就优先渲染render了
        const count = ref(0)

        function inc() { // 需要了解onClick事件系统
          count.value++
          click()
        }

        // 这里的任何东西 都不会被本身effect收集 只有return 后的方法 才会

        return () => // 这里还可以当redner来使用 我平时会的只是 return {}  template所需要的事件或属性 所以是检查function 还是 object来判断的吧
          h(Child, { // 那是不是有一个props effect? 标记也要注意下
            foo: count.value + 1,
            id: 'test',
            class: 'c' + count.value,
            style: { color: count.value ? 'red' : 'green' },
            onClick: inc,
            'data-id': count.value + 1
          })
      },
      mounted() {
        console.log('?')
      }
    }

    const Child = { // 原来是这样传参数的 那么就不需要this 什么的了
      setup(props: any) {
        onUpdated(childUpdated)
        return () =>
          h(
            'div',
            {
              class: 'c2',
              style: { fontWeight: 'bold' }
            },
            props.foo // 这个为什么是undefinded呢 因为setFullProps执行的时候,判断到的赋值Props方式为attrs,所以instance.props为空
         )
      }
    }

    const root = document.createElement('div')
    document.body.appendChild(root)
    render(h(Hello), root) // 这个render 是'@vue/runtime-dom' 我们之前用的 是'@vue/runtime-test' 里面的 测试用的... 但是区别不一样的就是 会不会初始化而已

    const node = root.children[0] as HTMLElement

    expect(node.getAttribute('id')).toBe('test')
    expect(node.getAttribute('foo')).toBe('1')
    expect(node.getAttribute('class')).toBe('c2 c0')
    expect(node.style.color).toBe('green')
    expect(node.style.fontWeight).toBe('bold')
    expect(node.dataset.id).toBe('1')

    node.dispatchEvent(new CustomEvent('click')) // 事件触发a
    node.dispatchEvent(new CustomEvent('click')) // 事件触发a
    expect(click).toHaveBeenCalled()

    await nextTick()
    expect(childUpdated).toHaveBeenCalled()
    expect(node.getAttribute('id')).toBe('test')
    expect(node.getAttribute('foo')).toBe('2')
    expect(node.getAttribute('class')).toBe('c2 c1')
    expect(node.style.color).toBe('red')
    expect(node.style.fontWeight).toBe('bold')
    expect(node.dataset.id).toBe('2')
  })

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

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

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

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

<template>
  <div id="1">123</div>
</template>

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

渲染过程

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

render

入口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的传入?

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

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

wocao什么,还不懂?那从头看一遍,懂了的可以继续往下看render到底做了啥。

shapeFlage

把②参数命名为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为一个常数。

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

托脸想了解这套运行机制麽?上面被normalizeChildren过的, 是按位与的意思,比如二进制中

01 | 01 === 01
01 | 10 === 11
10 | 10 === 10

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

判断类型的按位且

11 & 11 === 11
01 & 10 === 00
10 & 10 === 10

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

有没有注意到上面typescript的ShapeFlags的枚举,后面有二进制注释,有没有注意到判断类型渲染,都是使用if来判断的,就是说只要大于0就行。 有没有发现了什么?再提醒一点,二进制标记法?没错normalizeChildren中, 只不过是两个类型利用二进制标记的合并,比如100代表STATEFUL_COMPONENT类型,10000代表ARRAY_CHILDREN类型,两个使用100 | 10000是不是等于10100,而在

patchif(vnode.shapeFlag & ShapeFlags.COMPONENT)

ShapeFlags.COMPONENT为110,再看看那个表,是不是FUNCTIONAL_COMPONENT | STATEFUL_COMPONENT可以得到110? 所以10100 & 110为true,以上方法就是通过|来进行两种类型二进制占位符来合并,通过&判断该位置的值是否存在。

bailefolun是不是顿时大悟,感觉自己的代码质量大升,下次写代码也可以通过二进制标记来进行类型判断了!学废了没?那继续下面的东西。

processComponent

processComponent,判断到n1 == null && n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE, 则执行mountComponent,这个东西作用是挂载组件,这里挂载组件分三步走:

  • createComponentInstance 创建instance,一个组件相关的Object。
  • setupComponent,对instance.attrs、vnode.type.setup和vnode.type中的所有关键OPTIONS字段的处理。
  • setupRenderEffect,处理instance.render、instance.vnode.type、instance.subTree、更新组件的effect和instance.subTree.props合并instance.attrs。

①createComponentInstance

没有做什么,就是像vnode一样,生成了一个对象instance,该对象记录了当前组件的信息,比如parent字段为父组件的instance, root根组件的instance,render根据STATEFUL_COMPONENT、FUNCTIONAL_COMPONENT所生成的一个返回vnode的函数,也会记录一些生命周期的钩子相关字段。

②setupComponent

initProps:

// 创建了一个props和attrs的数组:
const props = []
const attrs = []

setFullProps(instance, instance.vnode.props, props, attrs),就是处理props啦,什么,你问这个东西做了什么?

熊猫紧张

1.normalizePropsOptions,我们组件中,如果存在props字段。

const a = {
  props: ['a-b', 'c-d']
}
const b = {
  props: { 
    foo: Boolean,
    test: {
        type: Number,
        default: 0
    }
  }
}

const normalized = {}
const needCastKeys = []

如果拿a做举例,检测到props是一个数组,看代码输出的结果你就明白了。

托脸以下是normalized和needCastKeys,拿a做例子,输出的结果:

const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
  ? Object.freeze({})
  : {}

// 输出的结果
normalized = {
    'aB': EMPTY_OBJ,
    'cD': EMPTY_OBJ
}
// 输出的结果
needCastKeys = undefined

b做举例,检测到是对象,遍历props,把遍历键key转换为驼峰式写法, 检测到当遍历键key为'foo'时,遍历值value为Boolean类型,needCastKeys.push(key),检测到test拥有default字段,

needCastKeys.push(key)。下面这个代码可以不看。

for(const key in instance.props) {
  const opt = raw[key]
  const prop: NormalizedProp = (normalized[normalizedKey] =
          isArray(opt) || isFunction(opt) ? { type: opt } : opt)
  if (prop) {
    const booleanIndex = getTypeIndex(Boolean, prop.type) // 检测boolean类型,如果是数组返回第一个index否则返回0
    const stringIndex = getTypeIndex(String, prop.type) // 检测string类型
    prop[BooleanFlags.shouldCast] = booleanIndex > -1
    prop[BooleanFlags.shouldCastTrue] =
      stringIndex < 0 || booleanIndex < stringIndex
    // if the prop needs boolean casting or default value
    if (booleanIndex > -1 || hasOwn(prop, 'default')) {
      needCastKeys.push(normalizedKey)
    }
  }
}

最终输出:

normalized = {
  foo: {
    type: Boolean
  },
  test: {
    type: Number,
    default: 0
  }
}
normalized.foo[BooleanFlags.shouldCast] = true
normalized.foo[BooleanFlags.shouldCastTrue] = true

normalized.test[BooleanFlags.shouldCast] = true
normalized.test[BooleanFlags.shouldCastTrue] = true
needCastKeys = ['foo', 'test']

这里有一个是instance.type.mixins、instance.type.mixins和全局minxins的循环调用normalizePropsOptions,最终是会合并成一整个:

const normalizedEntry: NormalizedPropsOptions = [normalized, needCastKeys]
return normalizedEntry

2.经过1得到[normalized, needCastKeys]

setFullProps

最后根据[normalized, needCastKeys] 遍历vnode.props来赋值給props或是attrs。 还记得一开始我们创建的props,attrs麽?以上都是为了设置这两个的。

3.如果有needCastKeys,这里normalized改一个名称为options

if (needCastKeys) {
    const rawCurrentProps = toRaw(props) // 避免有引用属性的响应式的干扰
    for (let i = 0; i < needCastKeys.length; i++) {
      const key = needCastKeys[i]
      props[key] = resolvePropValue(
        options!,
        rawCurrentProps,
        key,
        rawCurrentProps[key]
      )
    }
  }

我们现在还是用上面的1中的b做例子,这里先说Boolean的。这里的例子因为props没有test这个字段,所以设置默认值为false。

// 参数名称:
resolvePropValue(
  options: NormalizedPropsOptions[0],
  props: Data,
  key: string,
  value: unknown
)


const opt = options[key] as any
const hasDefault = hasOwn(opt, 'default')

// boolean casting
    if (opt[BooleanFlags.shouldCast]) {
      if (!hasOwn(props, key) && !hasDefault) {
        value = false
      } else if (
        opt[BooleanFlags.shouldCastTrue] &&
        (value === '' || value === hyphenate(key))
      ) {
        value = true
      }
    }
return value // 执行到这里

再说一下default的,没有value且有默认这个字段,检测到default不是function而是一个值,所以直接防护default的值0.

// 参数名称:
resolvePropValue(
  options: NormalizedPropsOptions[0],
  props: Data,
  key: string,
  value: unknown
)

const opt = options[key] as any
const hasDefault = hasOwn(opt, 'default')

// default values
    if (hasDefault && value === undefined) {
      const defaultValue = opt.default
      value =
        opt.type !== Function && isFunction(defaultValue)
          ? defaultValue()
          : defaultValue
    }
return value

看不懂?

就这么说吧,现在有两个数组一个为props一个为attrs,通过setFullProps,根据vnode.type.props进行normalizePropsOptions标准化生成[options, needCastKeys], options的生成是根据为Object的类型的vnode.type.props而定的。当vnode.type.props的类型为Object时,needCastKeys = Object.keys(vnode.type.props),当vnode.type.props的类型为数组时,needCastKeys = vnode.type.props,当然这里的key的名称都是会进行驼峰化。 在得到options和needCastKeys后,

存在options时,遍历vnode.props,但是跳过键key === 'key' || key === 'ref'的遍历,当键key存在options中,则camelKey = camelize(key)props[camelKey] = value遍历的值, 否则当不存在instance.emit或者该键key不是instance.emit的参数时,attrs[key] = value,key还是原来的键key。

存在needCastKeys时,遍历needCastKeys,value使用resolvePropValue,根据options与value是否为空、是否为空字符串之类处理成默认值或者布尔值,都没有就直接返回不经过处理的value。

熊猫紧张什么?你又想让我給你科普一下emit?下次一定下次一定!行了,点开emit章节,你就能了解欸。

最后设置instance的引用。

if (isStateful) {
  // stateful
  instance.props = isSSR ? props : shallowReactive(props) // 所以props一层响应
} else {
  if (!instance.type.props) {
    // functional w/ optional props, props === attrs
    instance.props = attrs
  } else {
    // functional w/ declared props
    instance.props = props
  }
}
instance.attrs = attrs

熊猫紧张流程图有... 写得很明白,希望你能去看几眼。

instance.attrs 将会被subTree.props合并,subTree就是instance.render()返回的vnode。

instance.props 状态类型组件(ShapeFlags.STATEFUL_COMPONENT)在运行instance.setup中传入的是instance.proxy,只会在instance.render(instance.props)应用到。 函数类型(ShapeFlags.FUNCTIONAL_COMPONENT)的组件,在setupRenderEffect中的renderComponentRoot调用vnode.type的时候传入instance.proxy。

instance.attrs用于与子(instance.subTree)vnode.props合并,instance.props用于传递,比如在ShapeFlags.STATEFUL_COMPONENT类型的render中作为参数,ShapeFlags.FUNCTIONAL_COMPONENT类型中的type作为参数。 调用组件setup的参数是(instance.props, instance.setupContext)。 这里需要好好区别一下。

attrs和props的本质区别是,如果instance.type.props存在,当遍历vnode.props的时候,赋值給propss得方式为键key存在于instance.type.props中的,如果键key不存在于instance.type.props 且instance.type.emit没有用到该键key,则赋值給attrs。needCastKeys是用来給props设置一下默认字段和布尔字段的,使其标准化一些。

一句话,attrs是被instance.type.props所过滤的vnode.props。

initSlots

这里的测试用例没有slots,相关移步去slots章节。

setupStatefulComponent: ShapeFlags.STATEFUL_COMPONENT才会执行,当前组件为ShapeFlags.STATEFUL_COMPONENT类型。设置instance.accessCache和instance.proxy,instance.proxy在处理options,或者说options中使用的this,将指向instance.proxy。

instance.accessCache = {}
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
// options:
{
  methods: { 
      foo() { 
          console.log(this)
     } 
  }
}

// output: 
// instance.proxy

instance.type.setup的运行

currentInstance = instance // 使用钩子api的时候会使用到。
pauseTracking() // 暂停track,reactive相关,这里停止track是避免执行setup方法的时候,有响应式数据追踪到其他地方的effect,比如具有父组件进行componentEffect的过程中会patch子组件,子组件更新,父组件也需要更新,这一步仅仅在componentEffect中处理。

setupResult = callWithErrorHandling // 调用组件的setup方法,传入(instance.props, instance.setupConetxt)。

resetTracking() // 恢复上一个track的状态,reactive相关
currentInstance = null // 恢复

callWithErrorHandling包裹setup方法,执行setup(instance.props, instance.setupContext)

setup内部钩子相关:

当前测试用例调用了onUpdated(CLICKEVENT)钩子,onUpdated所做的事是:

instance.u = []
instance.u.push(CLICKEVENT)

代码:

// 创建钩子:
createHook(LifecycleHooks.UPDATED)

// createHook内部:
injectHook(LifecycleHooks.UPDATED, CLICKEVENT, currentInstance)

// injectHook内部:
const hooks = target[type] || (target[type] = [])
const wrappedHook =
hook.__weh || hook.__weh = (...args: unknown[]) => {
      // 暂不讨论...
}
hooks.push(wrappedHook)

运行以上后,返回setup的结果給setupResult。执行handleSetupResult

setupResult是一个Function:

instance.render = setupResult // 当前组件返回的是组件类型

如果setupResult是一个Object类型:

instance.setupState = reactive(setupResult) // 进行响应式

setupResult为Object类型的时候,进行响应式化有什么好处?(其实我是感觉防止新手不知道怎么处理吧)我们平时会直接返回一个对象,对象里面包裹响应式数据对象,如果再套一层响应式化,可以让我们直接设置字段, 输入数据,不需要再进行一次relative或者ref的调用,这和处理instance.type.data同一个原理,

也就是说你可以直接在setup中的return直接当data(){}那样返回

这里再提醒一下,你需要打开渲染流程图,才能知道流程走向到底去哪了。

执行finishComponentSetup,这里就涉及到options(methids, props, data, mixin, watch, computed...)的设置了。具体去测试用例调试吧,如果需要特别说一下可以在Github提一下issus,这里说几个重点。

callSyncHook('beforeCreate', instance.type.options),调用全局mixin、extends、本身mixin,最后才是调用自身的。 这里会有两个钩子被执行'beforeCreate'和'created',这两个钩子是使用composition API是没有的,这里的钩子比setup中使用api的钩子要提早执行。 如果内部方法使用this,那么都会指向instance.proxy,详情可以查看PublicInstanceProxyHandlers。

③setupRenderEffect

sucide终于要看到重点了,这个就分开说吧,因为涉及到的东西很多了。

results matching ""

    No results matching ""