4.3 Vnode的创建

先简单回顾一下挂载的流程,挂载的过程调用的是Vue实例上$mount方法,而$mount的核心是mountComponent方法。在这之前,如果我们传递的是template模板,会经过一系列的模板编译过程,并根据不同平台生成对应代码,浏览器对应的是render函数;如果传递的是render函数,则忽略模板编译过程。有了render函数后,调用vm._render()方法会将render函数转化为Virtual DOM,最终利用vm._update()Virtual DOM渲染为真实的DOM

  1. Vue.prototype.$mount = function(el, hydrating) {
  2. ···
  3. return mountComponent(this, el)
  4. }
  5. function mountComponent() {
  6. ···
  7. updateComponent = function () {
  8. vm._update(vm._render(), hydrating);
  9. };
  10. }

vm._render()方法会将render函数转化为Virtual DOM,我们看源码中如何定义的。

  1. // 引入Vue时,执行renderMixin方法,该方法定义了Vue原型上的几个方法,其中一个便是 _render函数
  2. renderMixin();//
  3. function renderMixin() {
  4. Vue.prototype._render = function() {
  5. var ref = vm.$options;
  6. var render = ref.render;
  7. ···
  8. try {
  9. vnode = render.call(vm._renderProxy, vm.$createElement);
  10. } catch (e) {
  11. ···
  12. }
  13. ···
  14. return vnode
  15. }
  16. }

抛开其他代码,_render函数的核心是render.call(vm._renderProxy, vm.$createElement)部分,vm.$createElement方法会作为render函数的参数传入。这个参数也是在手写render函数时使用的createElement参数的由来

  1. new Vue({
  2. el: '#app',
  3. render: function(createElement) {
  4. return createElement('div', {}, this.message)
  5. },
  6. data() {
  7. return {
  8. message: 'dom'
  9. }
  10. }
  11. })

vm.$createElementVueinitRender所定义的方法,其中 vm._ctemplate内部编译成render函数时调用的方法,vm.$createElement是手写render函数时调用的方法。两者的唯一区别是:内部生成的render方法可以保证子节点都是Vnode(下面有特殊的场景),而手写的render需要一些检验和转换。

  1. function initRender(vm) {
  2. vm._c = function(a, b, c, d) { return createElement(vm, a, b, c, d, false); }
  3. vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
  4. }

createElement 方法实际上是对 _createElement 方法的封装,在调用_createElement创建Vnode之前,会对传入的参数进行处理。例如当没有data数据时,参数会往前填充。

  1. function createElement (
  2. context, // vm 实例
  3. tag, // 标签
  4. data, // 节点相关数据,属性
  5. children, // 子节点
  6. normalizationType,
  7. alwaysNormalize // 区分内部编译生成的render还是手写render
  8. ) {
  9. // 对传入参数做处理,可以没有data,如果没有data,则将第三个参数作为第四个参数使用,往上类推。
  10. if (Array.isArray(data) || isPrimitive(data)) {
  11. normalizationType = children;
  12. children = data;
  13. data = undefined;
  14. }
  15. // 根据是alwaysNormalize 区分是内部编译使用的,还是用户手写render使用的
  16. if (isTrue(alwaysNormalize)) {
  17. normalizationType = ALWAYS_NORMALIZE;
  18. }
  19. return _createElement(context, tag, data, children, normalizationType) // 真正生成Vnode的方法
  20. }

4.3.1 数据规范检测

Vue既然暴露给用户用render函数去写渲染模板,就需要考虑用户操作带来的不确定性,因此在生成Vnode的过程中,_createElement会先进行数据规范的检测,将不合法的数据类型错误提前暴露给用户。接下来将列举几个容易犯错误的实际场景,方便理解源码中如何处理这类错误的。

    • 用响应式对象做节点属性
  1. new Vue({
  2. el: '#app',
  3. render: function (createElement, context) {
  4. return createElement('div', this.observeData, this.show)
  5. },
  6. data() {
  7. return {
  8. show: 'dom',
  9. observeData: {
  10. attr: {
  11. id: 'test'
  12. }
  13. }
  14. }
  15. }
  16. })
    • 特殊属性key为非字符串,数字类型
  1. new Vue({
  2. el: '#app',
  3. render: function(createElement) {
  4. return createElement('div', { key: this.lists }, this.lists.map(l => {
  5. return createElement('span', l.name)
  6. }))
  7. },
  8. data() {
  9. return {
  10. lists: [{
  11. name: '111'
  12. },
  13. {
  14. name: '222'
  15. }
  16. ],
  17. }
  18. }
  19. })

这些规范都会在创建Vnode节点之前发现并报错,源代码如下:

  1. function _createElement (context,tag,data,children,normalizationType) {
  2. // 数据对象不能是定义在Vue data属性中的响应式数据。
  3. if (isDef(data) && isDef((data).__ob__)) {
  4. warn(
  5. "Avoid using observed data object as vnode data: " + (JSON.stringify(data)) + "\n" +
  6. 'Always create fresh vnode data objects in each render!',
  7. context
  8. );
  9. return createEmptyVNode() // 返回注释节点
  10. }
  11. // 针对动态组件 :is 的特殊处理,组件相关知识放到特定章节分析。
  12. if (isDef(data) && isDef(data.is)) {
  13. tag = data.is;
  14. }
  15. if (!tag) {
  16. // 防止动态组件 :is 属性设置为false时,需要做特殊处理
  17. return createEmptyVNode()
  18. }
  19. // key值只能为string,number这些原始数据类型
  20. if (isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  21. ) {
  22. {
  23. warn(
  24. 'Avoid using non-primitive value as key, ' +
  25. 'use string/number value instead.',
  26. context
  27. );
  28. }
  29. }
  30. ···
  31. // 省略后续操作
  32. }

4.3.2 子节点children规范化

Virtual DOM需要保证每一个子节点都是Vnode类型,这里分两种场景。

  • 1.render函数编译,理论上通过render函数编译生成的都是Vnode类型,但是有一个例外,函数式组件返回的是一个数组(关于组件,以及函数式组件内容,我们放到专门讲组件的时候专题分析),这个时候Vue的处理是将整个children拍平。
  • 2.用户定render函数,这个时候也分为两种情况,一个是chidren为文本节点,这时候通过前面介绍的createTextVNode 创建一个文本节点的 VNode; 另一种相对复杂,当children中有v-for的时候会出现嵌套数组,这时候的处理逻辑是,遍历children,对每个节点进行判断,如果依旧是数组,则继续递归调用,直到类型为基础类型时,调用createTextVnode方法转化为Vnode。这样经过递归,children变成了一个类型为Vnode的数组。
  1. function _createElement() {
  2. ···
  3. if (normalizationType === ALWAYS_NORMALIZE) {
  4. // 用户定义render函数
  5. children = normalizeChildren(children);
  6. } else if (normalizationType === SIMPLE_NORMALIZE) {
  7. // render 函数是编译生成的
  8. children = simpleNormalizeChildren(children);
  9. }
  10. }
  11. // 处理编译生成的render 函数
  12. function simpleNormalizeChildren (children) {
  13. for (var i = 0; i < children.length; i++) {
  14. // 子节点为数组时,进行开平操作,压成一维数组。
  15. if (Array.isArray(children[i])) {
  16. return Array.prototype.concat.apply([], children)
  17. }
  18. }
  19. return children
  20. }
  21. // 处理用户定义的render函数
  22. function normalizeChildren (children) {
  23. // 递归调用,直到子节点是基础类型,则调用创建文本节点Vnode
  24. return isPrimitive(children)
  25. ? [createTextVNode(children)]
  26. : Array.isArray(children)
  27. ? normalizeArrayChildren(children)
  28. : undefined
  29. }
  30. // 判断是否基础类型
  31. function isPrimitive (value) {
  32. return (
  33. typeof value === 'string' ||
  34. typeof value === 'number' ||
  35. typeof value === 'symbol' ||
  36. typeof value === 'boolean'
  37. )
  38. }

=== 进行数据检测和组件规范化后,接下来通过new VNode便可以生成一棵`VNode树。===具体细节由于篇幅原因,不展开分析。