Vue(v2.6.11)万行源码生啃,就硬刚!(上)

简介: 众所周知,以下代码就是 vue 的一种直接上手方式。通过 cdn 可以在线打开 vue.js。一个文件,一万行源码,是万千开发者赖以生存的利器,它究竟做了什么?让人品味。

image.png


前言



源码阅读可能会迟到,但是一定不会缺席!


众所周知,以下代码就是 vue 的一种直接上手方式。通过 cdn 可以在线打开 vue.js。一个文件,一万行源码,是万千开发者赖以生存的利器,它究竟做了什么?让人品味。


<html>
<head></head>
<body>
    <div id="app">
        {{ message }}
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    var app = new Vue({
        el: '#app',
        data: {
            message: 'See Vue again!'
        },
    })
</script>
</html>


源码cdn地址:cdn.jsdelivr.net/npm/vue/dis…,当下版本:v2.6.11。


本瓜选择生啃的原因是,可以更自主地选择代码段分轻重来阅读,一方面测试自己的掌握程度,一方面追求更直观的源码阅读。


当然你也可以选择在 github.com/vuejs/vue/t… 分模块的阅读,也可以看各路大神的归类整理。


其实由于本次任务量并不算小,为了能坚持下来,本瓜将源码尽量按 500 行作为一个模块来形成一个 md 文件记录(分解版本共 24 篇感兴趣可移步),结合注释、自己的理解、以及附上对应查询链接来逐行细读源码,此篇为合并版本


目的:自我梳理,分享交流。


最佳阅读方式推荐:先点赞👍再阅读📖,靴靴靴靴😁


正文



第 1 行至第 10 行


// init

(
    function (global, factory) {
        typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.Vue = factory());
    }(
        this,
        function () {
            'use strict';
            //...核心代码...
        }
    )
);
// 变形
if (typeof exports === 'object' && typeof module !== 'undefined') { // 检查 CommonJS
    module.exports = factory()
} else {
    if (typeof define === 'function' && define.amd) { // AMD 异步模块定义 检查JavaScript依赖管理库 require.js 的存在 [link](https://stackoverflow.com/questions/30953589/what-is-typeof-define-function-defineamd-used-for)
        define(factory)
    } else {
        (global = global || self, global.Vue = factory());
    }
}
// 等价于
window.Vue=factory() 
// factory 是个匿名函数,该匿名函数并没自执行 设计参数 window,并传入window对象。不污染全局变量,也不会被别的代码污染


第 11 行至第 111 行


// 工具代码

var emptyObject = Object.freeze({});// 冻结的对象无法再更改 [link](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)

// 接下来是一些封装用来判断基本类型、引用类型、类型转换的方法

  • isUndef//判断未定义
  • isDef// 判断已定义
  • isTrue// 判断为 true
  • isFalse// 判断为 false
  • isPrimitive// 判断为原始类型
  • isObject// 判断为 obj
  • toRawType // 切割引用类型得到后面的基本类型,例如:[object RegExp] 得到的就是 RegExp
  • isPlainObject// 判断纯粹的对象:"纯粹的对象",就是通过 { }、new Object()、Object.create(null) 创建的对象
  • isRegExp// 判断原生引用类型
  • isValidArrayIndex// 检查val是否是一个有效的数组索引,其实就是验证是否是一个非无穷大的正整数
  • isPromise// 判断是否是 Promise
  • toString// 类型转成 String
  • toNumber// 类型转成 Number


第 113 行至第 354 行


  • makeMap// makeMap 方法将字符串切割,放到map中,用于校验其中的某个字符串是否存在(区分大小写)于map中 e.g.


var isBuiltInTag = makeMap('slot,component', true);// 是否为内置标签
isBuiltInTag('slot'); //true
isBuiltInTag('slot1'); //undefined
var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');// 是否为保留属性


  • remove// 数组移除元素方法
  • hasOwn// 判断对象是否含有某个属性
  • cached// ※高级函数 cached函数,输入参数为函数,返回值为函数。同时使用了闭包,其会将该传入的函数的运行结果缓存,创建一个cache对象用于缓存运行fn的运行结果。link


function cached(fn) {
    var cache = Object.create(null);// 创建一个空对象
    return (function cachedFn(str) {// 获取缓存对象str属性的值,如果该值存在,直接返回,不存在调用一次fn,然后将结果存放到缓存对象中
        var hit = cache[str];
        return hit || (cache[str] = fn(str))
    })
}


  • camelize// 驼峰化一个连字符连接的字符串
  • capitalize// 对一个字符串首字母大写
  • hyphenateRE// 用字符号连接一个驼峰的字符串
  • polyfillBind// ※高级函数 参考link
  • Function.prototype.bind() // link1link2
  • toArray// 将像数组的转为真数组
  • extend// 将多个属性插入目标的对象
  • toObject// 将对象数组合并为单个对象。


e.g.


console.log(toObject(["bilibli"]))
//{0: "b", 1: "i", 2: "l", 3: "i", 4: "b", 5: "l", 6: "i", encodeHTML: ƒ}


  • no// 任何情况都返回false
  • identity // 返回自身
  • genStaticKeys// 从编译器模块生成包含静态键的字符串。TODO:demo
  • looseEqual//※高级函数 对对象的浅相等进行判断

//有赞、头条面试题


function looseEqual(a, b) {
    if (a === b) return true
    const isObjectA = isObject(a)
    const isObjectB = isObject(b)
    if(isObjectA && isObjectB) {
        try {
            const isArrayA = Array.isArray(a)
            const isArrayB = Array.isArray(b)
            if(isArrayA && isArrayB) {
                return a.length === b.length && a.every((e, i) => {
                    return looseEqual(e, b[i])
                })
            }else if(!isArrayA && !isArrayB) {
                const keysA = Object.keys(a)
                const keysB = Object.keys(b)
                return keysA.length === keysB.length && keysA.every(function (key) {
                    return looseEqual(a[key], b[key])
                })
            }else {
                return false
            }
        } catch(e) {
            return false
        }
    }else if(!isObjectA && !isObjectB) {
        return String(a) === String(b)
    }else {
        return false
    }
}


  • looseIndexOf// 返回索引,如果没找到返回-1,否则执行looseEqual()
  • once// 确保函数只被调用一次,用到闭包


阶段小结


  • cached
  • polyfillBind
  • looseEqual


这三个函数要重点细品!主要的点是:闭包、类型判断,函数之间的互相调用。也即是这部分工具函数的精华!


第 356 行 至 第 612 行


// 定义常量和配置


  • SSR_ATTR// 服务端渲染
  • ASSET_TYPES// 全局函数 component、directive、filter
  • LIFECYCLE_HOOKS// 生命周期,无需多言
  • config // 全局配置 link
  • unicodeRegExp//用于解析html标记、组件名称和属性pat的unicode字母
  • isReserved//  检查变量的开头是 $ 或 _
  • def// 在一个对象上定义一个属性的构造函数,其中 !!enumerable 强制转换 boolean
  • parsePath// 解析一个简单路径 TODO:
  • userAgent// 浏览器识别
  • inBrowser
  • _isServer//检测 vue的服务器渲染是否存在, 而且避免webpack去填充process
  • isNative //这里判断 函数是否是系统函数, 比如 Function Object ExpReg window document 等等, 这些函数应该使用c/c++实现的。这样可以区分 Symbol是系统函数, 还是用户自定义了一个Symbol
  • hasSymbol//这里使用了ES6的Reflect方法, 使用这个对象的目的是, 为了保证访问的是系统的原型方法, ownKeys 保证key的输出顺序, 先数组 后字符串
  • _Set// 设置一个Set


link


第 616 行至第 706 行


//设置warn,tip等全局变量 TODO:

  • warn
  • tip
  • generateComponentTrace// 生成组件跟踪路径(组件数规则)
  • formatComponentName// 格式化组件名


第 710 行至第 763 行


Vue核心:数据监听最重要之一的 Dep


// Dep是订阅者Watcher对应的数据依赖
var Dep = function Dep () {
  //每个Dep都有唯一的ID
  this.id = uid++;
  //subs用于存放依赖
  this.subs = [];
};
//向subs数组添加依赖
Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
};
//移除依赖
Dep.prototype.removeSub = function removeSub (sub) {
  remove(this.subs, sub);
};
//设置某个Watcher的依赖
//这里添加了Dep.target是否存在的判断,目的是判断是不是Watcher的构造函数调用
//也就是说判断他是Watcher的this.get调用的,而不是普通调用
Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};
Dep.prototype.notify = function notify () {
  var subs = this.subs.slice();
  //通知所有绑定 Watcher。调用watcher的update()
  for (var i = 0, l = subs.length; i &lt; l; i++) {
    subs[i].update();
  }
};


强烈推荐阅读:link


Dep 相当于把 Observe 监听到的信号做一个收集(collect dependencies),然后通过dep.notify()再通知到对应 Watcher ,从而进行视图更新。


第 767 行至第 900 行


Vue核心:视图更新最重要的 VNode( Virtual DOM)


  • VNode
  • createEmptyVNode
  • createTextVNode
  • cloneVNode


把你的 template 模板 描述成 VNode,然后一系列操作之后通过 VNode 形成真实DOM进行挂载


更新的时候对比旧的VNode和新的VNode,只更新有变化的那一部分,提高视图更新速度。


e.g.

<div class="parent" style="height:0" href="2222">
    111111
</div>
//转成Vnode
{    
    tag: 'div',    
    data: {        
        attrs:{href:"2222"}
        staticClass: "parent",        
        staticStyle: {            
            height: "0"
        }
    },    
    children: [{        
        tag: undefined,        
        text: "111111"
    }]
}


强烈推荐阅读:link

  • methodsToPatch


将数组的基本操作方法拓展,实现响应式,视图更新。

因为:对于对象的修改是可以直接触发响应式的,但是对数组直接赋值,是无法触发的,但是用到这里经过改造的方法。我们可以明显的看到 ob.dep.notify() 这一核心。


阶段小结


这一 part 最重要的,毋庸置疑是:Dep 和 VNode,需重点突破!!!


第 904 行至第 1073 行


Vue核心:数据监听最重要之一的 Observer


  • 核心的核心!Observer(发布者) => Dep(订阅器) => Watcher(订阅者)

类比一个生活场景:报社将各种时下热点的新闻收集,然后制成各类报刊,发送到每家门口的邮箱里,订阅报刊人们看到了新闻,对新闻作出评论。

在这个场景里,报社==发布者,新闻==数据,邮箱==订阅器,订阅报刊的人==订阅者,对新闻评论==视图更新


  • Observer//Observer的调用过程:initState()-->observe(data)-->new Observer()
var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, '__ob__', this);
  if (Array.isArray(value)) {
    if (hasProto) {
      protoAugment(value, arrayMethods);
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};


  • ※※ defineReactive 函数,定义一个响应式对象,给对象动态添加 getter 和 setter ,用于依赖收集和派发更新。


function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()// 1. 为属性创建一个发布者
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // cater for pre-defined getter/setters
  const getter = property && property.get // 依赖收集
  const setter = property && property.set // 派发更新
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  let childOb = !shallow && observe(val)// 2. 获取属性值的__ob__属性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()// 3. 添加 Dep
        if (childOb) {
          childOb.dep.depend()//4. 也为属性值添加同样的 Dep 
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}


第 4 步非常重要。为对象的属性添加 dep.depend(),达到监听对象(引用的值)属性的目的


重点备注


Vue对数组的处理跟对象还是有挺大的不同,length是数组的一个很重要的属性,无论数组增加元素或者删除元素(通过splice,push等方法操作)length的值必定会更新,为什么不直接操作监听length呢?而需要拦截splice,push等方法进行数组的状态更新?


原因是:在数组length属性上用defineProperty拦截的时候,会报错。


Uncaught TypeError: Cannot redefine property: length
复制代码


再用Object.getOwnPropertyDescriptor(arr, 'length')查看一下://(Object.getOwnPropertyDescriptor用于返回defineProperty.descriptor)

{ configurable: false enumerable: false value: 0 writable: true } configurable为false,且MDN上也说重定义数组的length属性在不同浏览器上表现也是不一致的,所以还是老老实实拦截splice,push等方法,或者使用ES6的Proxy。


第 1075 行至第 1153 行


  • set //在对象上设置一个属性。如果是新的属性就会触发更改通知(旧属性也会触发更新通知,因为第一个添加的时候已经监听了,之后自动触发,不再手动触发)
  • del //删除一个属性,如果必要触发通知
  • dependArray // 收集数组的依赖link


第 1157 行至第 1568 行


// 配置选项合并策略


ar strats = config.optionMergeStrategies;
复制代码
  • mergeData
  • strats.data
  • mergeDataOrFn
  • mergeHook
  • mergeAssets
  • strats.watch
  • strats.computed
  • defaultStrat
  • checkComponents
  • validateComponentName
  • normalizeProps
  • normalizeInject
  • normalizeDirectives
  • assertObjectType
  • mergeOptions


这一部分代码写的就是父子组件配置项的合并策略,包括:默认的合并策略、钩子函数的合并策略、filters/props、data合并策略,且包括标准的组件名、props写法有一个统一化规范要求。


一图以蔽之


image.png


强烈推荐阅读:link


阶段小结


这一部分最重要的就是 Observer(观察者) ,这也是 Vue 核心中的核心!其次是 mergeOptions(组件配置项的合并策略),但是通常在用的过程中,就已经了解到了大部分的策略规则。


第 1570 行至第 1754 行


  • resolveAsset// resolveAsset 全局注册组件用到


e.g.

我们的调用 resolveAsset(context.image.pngoptions.components[tag],这样我们就可以在 resolveAsset 的时候拿到这个组件的构造函数,并作为 createComponent 的钩子的参数。


  • validateProp// prop的格式校验


校验prop:

  1. prop为Boolean类型时做特殊处理
  2. prop的值为空时,获取默认值,并创建观察者对象
  3. prop验证
  • getPropDefaultValue// 获取默认 prop 值


获取 prop 的默认值 && 创建观察者对象

  1. @param {*} vm vm 实例
  2. @param {*} prop 定义选项
  3. @param {*} vmkey prop 的 key


// 在非生产环境下(除去 Weex 的某种情况),将对prop进行验证,包括验证required、type和自定义验证函数。


  • assertProp //验证 prop Assert whether a prop is valid.
case 1: 验证 required 属性
   case 1.1: prop 定义时是 required,但是调用组件时没有传递该值(警告)
   case 1.2: prop 定义时是非 required 的,且 value === null || value === undefined(符合要求,返回)
case 2: 验证 type 属性-- value 的类型必须是 type 数组里的其中之一
case 3: 验证自定义验证函数


  • assertType
`assertType`函数,验证`prop`的值符合指定的`type`类型,分为三类:
  - 第一类:通过`typeof`判断的类型,如`String`、`Number`、`Boolean`、`Function`、`Symbol`
  - 第二类:通过`Object.prototype.toString`判断`Object`/`Array`
  - 第三类:通过`instanceof`判断自定义的引用类型


第 1756 行至第 1823 行


// 辅助函数:检测内置类型

  • getType
  • isSameType
  • getTypeIndex
  • getInvalidTypeMessage
  • styleValue
  • isExplicable
  • isBoolean


第 1827 行至第 1901 行


// 辅助函数:处理错误、错误打印

  • handleError
  • invokeWithErrorHandling
  • globalHandleError
  • logError


第 1905 行至第 2007 行


  • flushCallbacks// flushCallbacks 挨个同步执行callbacks中回调
  • MutationObserver
  • nextTick// 把传入的 cb 回调函数用 try-catch 包裹后放在一个匿名函数中推入callbacks数组中,这么做是因为防止单个 cb 如果执行错误不至于让整个JS线程挂掉,每个 cb 都包裹是防止这些回调函数如果执行错误不会相互影响,比如前一个抛错了后一个仍然可以执行。


精髓中的精髓 —— nextTick


这里有一段很重要的注释


// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
在vue2.5之前的版本中,nextTick基本上基于 micro task 来实现的,但是在某些情况下 micro task 具有太高的优先级,并且可能在连续顺序事件之间(例如#4521,#6690)或者甚至在同一事件的事件冒泡过程中之间触发(#6566)。但是如果全部都改成 macro task,对一些有重绘和动画的场景也会有性能影响,如 issue #6813。vue2.5之后版本提供的解决办法是默认使用 micro task,但在需要时(例如在v-on附加的事件处理程序中)强制使用 macro task。


什么意思呢?分析下面这段代码。


<span id='name' ref='name'>{{ name }}</span>
<button @click='change'>change name</button>
methods: {
      change() {
          this.$nextTick(() => console.log('setter前:' + this.$refs.name.innerHTML))
          this.name = ' vue3 '
          console.log('同步方式:' + this.$refs.name.innerHTML)
          setTimeout(() => this.console("setTimeout方式:" + this.$refs.name.innerHTML))
          this.$nextTick(() => console.log('setter后:' + this.$refs.name.innerHTML))
          this.$nextTick().then(() => console.log('Promise方式:' + this.$refs.name.innerHTML))
      }
  }
//同步方式:vue2
//setter前:vue2
//setter后: vue3 
//Promise方式: vue3 
//setTimeout方式: vue3 


  1. 同步方式: 当把data中的name修改之后,此时会触发name的 setter 中的 dep.notify 通知依赖本data的render watcher去 update,update 会把 flushSchedulerQueue 函数传递给 nextTick,render watcher在 flushSchedulerQueue 函数运行时 watcher.run 再走 diff -> patch 那一套重渲染 re-render 视图,这个过程中会重新依赖收集,这个过程是异步的;所以当我们直接修改了name之后打印,这时异步的改动还没有被 patch 到视图上,所以获取视图上的DOM元素还是原来的内容。
  2. setter前: setter前为什么还打印原来的是原来内容呢,是因为 nextTick 在被调用的时候把回调挨个push进callbacks数组,之后执行的时候也是 for 循环出来挨个执行,所以是类似于队列这样一个概念,先入先出;在修改name之后,触发把render watcher填入 schedulerQueue 队列并把他的执行函数 flushSchedulerQueue 传递给 nextTick ,此时callbacks队列中已经有了 setter前函数 了,因为这个 cb 是在 setter前函数 之后被push进callbacks队列的,那么先入先出的执行callbacks中回调的时候先执行 setter前函数,这时并未执行render watcher的 watcher.run,所以打印DOM元素仍然是原来的内容。
  3. setter后: setter后这时已经执行完 flushSchedulerQueue,这时render watcher已经把改动 patch 到视图上,所以此时获取DOM是改过之后的内容。
  4. Promise方式: 相当于 Promise.then 的方式执行这个函数,此时DOM已经更改。
  5. setTimeout方式: 最后执行macro task的任务,此时DOM已经更改。


备注:前文提过,在依赖收集原理的响应式化方法 defineReactive 中的 setter 访问器中有派发更新 dep.notify() 方法,这个方法会挨个通知在 dep 的 subs 中收集的订阅自己变动的 watchers 执行 update。


强烈推荐阅读:link


0 行 至 2000 行小结


0 至 2000 行主要的内容是:


  1. 工具代码
  2. 数据监听:Obeserve,Dep
  3. Vnode
  4. nextTick


第 2011 行至第 2232 行


  • perf// performance
  • initProxy// 代理对象是es6的新特性,它主要用来自定义对象一些基本操作(如查找,赋值,枚举等)。link


//proxy是一个强大的特性,为我们提供了很多"元编程"能力。


const handler = {
    get: function(obj, prop) {
        return prop in obj ? obj[prop] : 37;
    }
};
const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b);      // 1, undefined
console.log('c' in p, p.c); // false, 37


link

  • traverse// 遍历:_traverse 深度遍历,用于

traverse 对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher,且遍历过程中会把子响应式对象通过它们的 dep id 记录到 seenObjects,避免以后重复访问。


  • normalizeEvent// normalizeEvents是针对v-model的处理,例如在IE下不支持change事件,只能用input事件代替。
  • createFnInvoker// 在初始构建实例时,旧节点是不存在的,此时会调用createFnInvoker函数对事件回调函数做一层封装,由于单个事件的回调可以有多个,因此createFnInvoker的作用是对单个,多个回调事件统一封装处理,返回一个当事件触发时真正执行的匿名函数。
  • updateListeners// updateListeners的逻辑也很简单,它会遍历on事件对新节点事件绑定注册事件,对旧节点移除事件监听,它即要处理原生DOM事件的添加和移除,也要处理自定义事件的添加和移除,


阶段小结


Vue 的事件机制


第 2236 行至第 2422 行


  • mergeVNodeHook// 重点 合并 VNode

// 把 hook 函数合并到 def.data.hook[hookey] 中,生成新的 invoker,createFnInvoker 方法


// vnode 原本定义了 init、prepatch、insert、destroy 四个钩子函数,而 mergeVNodeHook 函数就是把一些新的钩子函数合并进来,例如在 transition 过程中合并的 insert 钩子函数,就会合并到组件 vnode 的 insert 钩子函数中,这样当组件插入后,就会执行我们定义的 enterHook 了。


  • extractPropsFromVNodeData// 抽取相应的从父组件上的prop
  • checkProp// 校验 Prop
// The template compiler attempts to minimize the need for normalization by
    // statically analyzing the template at compile time.
    // 模板编译器尝试用最小的需求去规范:在编译时,静态分析模板
    // For plain HTML markup, normalization can be completely skipped because the
    // generated render function is guaranteed to return Array<VNode>. There are
    // two cases where extra normalization is needed:
    // 对于纯 HTML 标签,可跳过标准化,因为生成渲染函数一定会会返回 Vnode Array.有两种情况,需要额外去规范
    // 1. When the children contains components - because a functional component
    // may return an Array instead of a single root. In this case, just a simple
    // normalization is needed - if any child is an Array, we flatten the whole
    // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
    // because functional components already normalize their own children.
    // 当子级包含组件时-因为功能组件可能会返回Array而不是单个根。在这种情况下,需要规范化-如果任何子级是Array,我们将整个具有Array.prototype.concat的东西。保证只有1级深度,因为功能组件已经规范了自己的子代。
    // 2. When the children contains constructs that always generated nested Arrays,
    // e.g. <template>, <slot>, v-for, or when the children is provided by user
    // with hand-written render functions / JSX. In such cases a full normalization
    // is needed to cater to all possible types of children values.
    // 当子级包含始终生成嵌套数组的构造时,例如<template>,<slot>,v-for或用户提供子代时,具有手写的渲染功能/ JSX。在这种情况下,完全归一化,才能满足所有可能类型的子代值。


Q:这一段话说的是什么意思呢?

A:归一化操作其实就是将多维的数组,合并转换成一个一维的数组。在 Vue 中归一化分为三个级别,


  1. 不需要进行归一化
  2. 只需要简单的归一化处理,将数组打平一层
  3. 完全归一化,将一个 N 层的 children 完全打平为一维数组


利用递归来处理的,同时处理了一些边界情况。


第 2426 行至第 2490 行


  • initProvide
  • initInjections
  • resolveInject


第 2497 行至第 2958 行


  • resolveSlots// Runtime helper for resolving raw children VNodes into a slot object.
  • isWhitespace
  • normalizeScopedSlots
  • normalizeScopedSlot
  • proxyNormalSlot
  • renderList// Runtime helper for rendering v-for lists.
  • renderSlot// Runtime helper for rendering <slot>
  • resolveFilter// Runtime helper for resolving filters
  • checkKeyCodes// Runtime helper for checking keyCodes from config.
  • bindObjectProps//  Runtime helper for merging v-bind="object" into a VNode's data.
  • renderStatic// Runtime helper for rendering static trees.
  • markOnce// Runtime helper for v-once.


这一部分讲的是辅助程序 —— Vue 的各类渲染方法,从字面意思中可以知道一些方法的用途,这些方法用在Vue生成的渲染函数中。


  • installRenderHelpers// installRenderHelpers 用于执行以上。


第 2962 行至第 3515 行


  • FunctionalRenderContext// 创建一个包含渲染要素的函数
  • createFunctionalComponent


函数式组件的实现


Ctor,                                       //Ctro:组件的构造对象(Vue.extend()里的那个Sub函数)
  propsData,                                  //propsData:父组件传递过来的数据(还未验证)
  data,                                       //data:组件的数据
  contextVm,                                  //contextVm:Vue实例 
  children                                    //children:引用该组件时定义的子节点
复制代码


// createFunctionalComponent 最后会执行我们的 render 函数

特注:Vue 组件是 Vue 的核心之一


组件分为:异步组件和函数式组件

这里就是函数式组件相关


Vue提供了一种可以让组件变为无状态、无实例的函数化组件。从原理上说,一般子组件都会经过实例化的过程,而单纯的函数组件并没有这个过程,它可以简单理解为一个中间层,只处理数据,不创建实例,也是由于这个行为,它的渲染开销会低很多。实际的应用场景是,当我们需要在多个组件中选择一个来代为渲染,或者在将children,props,data等数据传递给子组件前进行数据处理时,我们都可以用函数式组件来完成,它本质上也是对组件的一个外部包装。


函数式组件会在组件的对象定义中,将functional属性设置为true,这个属性是区别普通组件和函数式组件的关键。同样的在遇到子组件占位符时,会进入createComponent进行子组件Vnode的创建。**由于functional属性的存在,代码会进入函数式组件的分支中,并返回createFunctionalComponent调用的结果。**注意,执行完createFunctionalComponent后,后续创建子Vnode的逻辑不会执行,这也是之后在创建真实节点过程中不会有子Vnode去实例化子组件的原因。(无实例)


官方说明

  • cloneAndMarkFunctionalResult
  • mergeProps
  • componentVNodeHooks
  • createComponent // createComponent 方法创建一个组件的 VNode。这 createComponent 是创建子组件的关键


// 创建组件的 VNode 时,若组件是函数式组件,则其 VNode 的创建过程将与普通组件有所区别。


  • createComponentInstanceForVnode // link


推荐阅读:link


  • installComponentHooks // installComponentHooks就是把 componentVNodeHooks的钩子函数合并到data.hook中,,在合并过程中,如果某个时机的钩子已经存在data.hook中,那么通过执行mergeHook函数做合并勾子。
  • mergeHook$1
  • transformModel
  • createElement// 创建元素
  • _createElement
  • applyNS
  • registerDeepBindings
  • initRender // 初识渲染

link


阶段小结


这一部分主要是围绕 Vue 的组件的创建。Vue 将页面划分成各类的组件,组件思想是 Vue 的精髓之一。


相关实践学习
Serverless极速搭建Hexo博客
本场景介绍如何使用阿里云函数计算服务命令行工具快速搭建一个Hexo博客。
相关文章
|
4天前
|
JavaScript
Vue基础知识总结 4:vue组件化开发
Vue基础知识总结 4:vue组件化开发
|
4天前
|
存储 JavaScript
Vue 状态管理工具vuex
Vue 状态管理工具vuex
|
11天前
|
缓存 JavaScript UED
Vue 中实现组件的懒加载
【10月更文挑战第23天】组件的懒加载是 Vue 应用中提高性能的重要手段之一。通过合理运用动态导入、路由配置等方式,可以实现组件的按需加载,减少资源浪费,提高应用的响应速度和用户体验。在实际应用中,需要根据具体情况选择合适的懒加载方式,并结合性能优化的其他措施,以打造更高效、更优质的 Vue 应用。
|
10天前
|
JavaScript
如何在 Vue 中使用具名插槽
【10月更文挑战第25天】通过使用具名插槽,你可以更好地组织和定制组件的模板结构,使组件更具灵活性和可复用性。同时,具名插槽也有助于提高代码的可读性和可维护性。
14 2
|
10天前
|
JavaScript
Vue 中的插槽
【10月更文挑战第25天】插槽的使用可以大大提高组件的复用性和灵活性,使你能够根据具体需求在组件中插入不同的内容,同时保持组件的结构和样式的一致性。
12 2
|
10天前
|
前端开发 JavaScript 容器
在 vite+vue 中使用@originjs/vite-plugin-federation 模块联邦
【10月更文挑战第25天】模块联邦是一种强大的技术,它允许将不同的微前端模块组合在一起,形成一个统一的应用。在 vite+vue 项目中,使用@originjs/vite-plugin-federation 模块联邦可以实现高效的模块共享和组合。通过本文的介绍,相信你已经了解了如何在 vite+vue 项目中使用@originjs/vite-plugin-federation 模块联邦,包括安装、配置和使用等方面。在实际开发中,你可以根据自己的需求和项目的特点,灵活地使用模块联邦,提高项目的可维护性和扩展性。
|
11天前
|
JavaScript 前端开发 UED
vue 提高 tree shaking 的效果
【10月更文挑战第23天】提高 Vue 中 Tree shaking 的效果需要综合考虑多个因素,包括模块的导出和引用方式、打包工具配置、代码结构等。通过不断地优化和调整,可以最大限度地发挥 Tree shaking 的优势,为 Vue 项目带来更好的性能和用户体验。
|
11天前
|
缓存 JavaScript UED
Vue 中异步加载模块的方式
【10月更文挑战第23天】这些异步加载模块的方式各有特点和适用场景,可以根据项目的需求和架构选择合适的方法来实现模块的异步加载,以提高应用的性能和用户体验
|
11天前
|
JavaScript 测试技术 UED
解决 Vue 项目中 Tree shaking 无法去除某些模块
【10月更文挑战第23天】解决 Vue 项目中 Tree shaking 无法去除某些模块的问题需要综合考虑多种因素,通过仔细分析、排查和优化,逐步提高 Tree shaking 的效果,为项目带来更好的性能和用户体验。同时,持续关注和学习相关技术的发展,不断探索新的解决方案,以适应不断变化的项目需求。
|
12天前
|
JavaScript 搜索推荐 前端开发
Vue SSR 预渲染的广泛应用场景及其优势
【10月更文挑战第23天】Vue SSR 预渲染技术在众多领域都有着广泛的应用价值,可以显著提升网站的性能、用户体验和搜索引擎优化效果。随着技术的不断发展和完善,其应用场景还将不断拓展和深化
28 2