源码阅读可能会迟到,但是一定不会缺席!
众所周知,以下代码就是 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 篇感兴趣可移步),结合注释、自己的理解、以及附上对应查询链接来逐行细读源码,此篇为合并版本。
目的:自我梳理,分享交流。
最佳阅读方式推荐:先点赞👍再阅读📖,靴靴靴靴😁
// 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对象。不污染全局变量,也不会被别的代码污染// 工具代码
var emptyObject = Object.freeze({});// 冻结的对象无法再更改 [link](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)// 接下来是一些封装用来判断基本类型、引用类型、类型转换的方法
var isBuiltInTag = makeMap('slot,component', true);// 是否为内置标签
isBuiltInTag('slot'); //true
isBuiltInTag('slot1'); //undefined
var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');// 是否为保留属性function cached(fn) {
var cache = Object.create(null);// 创建一个空对象
return (function cachedFn(str) {// 获取缓存对象str属性的值,如果该值存在,直接返回,不存在调用一次fn,然后将结果存放到缓存对象中
var hit = cache[str];
return hit || (cache[str] = fn(str))
})
}e.g.
console.log(toObject(["bilibli"]))
//{0: "b", 1: "i", 2: "l", 3: "i", 4: "b", 5: "l", 6: "i", encodeHTML: ƒ}//有赞、头条面试题
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
}
}这三个函数要重点细品!主要的点是:闭包、类型判断,函数之间的互相调用。也即是这部分工具函数的精华!
// 定义常量和配置
//设置warn,tip等全局变量 TODO:
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 < l; i++) {
subs[i].update();
}
};强烈推荐阅读:link
Dep 相当于把 Observe 监听到的信号做一个收集(collect dependencies),然后通过dep.notify()再通知到对应 Watcher ,从而进行视图更新。
Vue核心:视图更新最重要的 VNode( Virtual DOM)
把你的 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
将数组的基本操作方法拓展,实现响应式,视图更新。
因为:对于对象的修改是可以直接触发响应式的,但是对数组直接赋值,是无法触发的,但是用到这里经过改造的方法。我们可以明显的看到 ob.dep.notify() 这一核心。
这一 part 最重要的,毋庸置疑是:Dep 和 VNode,需重点突破!!!
Vue核心:数据监听最重要之一的 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);
}
};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。
// 配置选项合并策略
ar strats = config.optionMergeStrategies;这一部分代码写的就是父子组件配置项的合并策略,包括:默认的合并策略、钩子函数的合并策略、filters/props、data合并策略,且包括标准的组件名、props写法有一个统一化规范要求。
一图以蔽之

强烈推荐阅读:link
这一部分最重要的就是 Observer(观察者) ,这也是 Vue 核心中的核心!其次是 mergeOptions(组件配置项的合并策略),但是通常在用的过程中,就已经了解到了大部分的策略规则。
e.g.
我们的调用 resolveAsset(context.
options.components[tag],这样我们就可以在 resolveAsset 的时候拿到这个组件的构造函数,并作为 createComponent 的钩子的参数。
校验prop:
获取 prop 的默认值 && 创建观察者对象
// 在非生产环境下(除去 Weex 的某种情况),将对prop进行验证,包括验证required、type和自定义验证函数。
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`函数,验证`prop`的值符合指定的`type`类型,分为三类:
- 第一类:通过`typeof`判断的类型,如`String`、`Number`、`Boolean`、`Function`、`Symbol`
- 第二类:通过`Object.prototype.toString`判断`Object`/`Array`
- 第三类:通过`instanceof`判断自定义的引用类型// 辅助函数:检测内置类型
// 辅助函数:处理错误、错误打印
精髓中的精髓 —— 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 备注:前文提过,在依赖收集原理的响应式化方法 defineReactive 中的 setter 访问器中有派发更新 dep.notify() 方法,这个方法会挨个通知在 dep 的 subs 中收集的订阅自己变动的 watchers 执行 update。
强烈推荐阅读:link
0 至 2000 行主要的内容是:
//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, 37traverse 对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher,且遍历过程中会把子响应式对象通过它们的 dep id 记录到 seenObjects,避免以后重复访问。
// 把 hook 函数合并到 def.data.hook[hookey] 中,生成新的 invoker,createFnInvoker 方法
// vnode 原本定义了 init、prepatch、insert、destroy 四个钩子函数,而 mergeVNodeHook 函数就是把一些新的钩子函数合并进来,例如在 transition 过程中合并的 insert 钩子函数,就会合并到组件 vnode 的 insert 钩子函数中,这样当组件插入后,就会执行我们定义的 enterHook 了。
// 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 中归一化分为三个级别,
利用递归来处理的,同时处理了一些边界情况。
<slot>这一部分讲的是辅助程序 —— Vue 的各类渲染方法,从字面意思中可以知道一些方法的用途,这些方法用在Vue生成的渲染函数中。
函数式组件的实现
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去实例化子组件的原因。(无实例)
// 创建组件的 VNode 时,若组件是函数式组件,则其 VNode 的创建过程将与普通组件有所区别。
推荐阅读:link
这一部分主要是围绕 Vue 的组件的创建。Vue 将页面划分成各类的组件,组件思想是 Vue 的精髓之一。
几乎所有JS框架或插件的编写都有一个类似的模式,即向全局输出一个类或者说构造函数,通过创建实例来使用这个类的公开方法,或者使用类的静态全局方法辅助实现功能。相信精通Jquery或编写过Jquery插件的开发者会对这个模式非常熟悉。Vue.js也如出一辙,只是一开始接触这个框架的时候对它所能实现的功能的感叹盖过了它也不过是一个内容较为丰富和精致的大型类的本质。
这里要对 js 的继承有一个深刻的理解。 link
function Animal(){
this.live=true;
}
function Dog(name){
this.name=name
}
Dog.prototype=new Animal()
var dog1=new Dog("wangcai")
console.log(dog1)// Dog {name: "wangcai"}
console.log(dog1.live)// truefunction Animal(name,color){
this.name=name;
this.color=color;}
function Dog(){
Animal.apply(this,arguments)
}
var dog1=new Dog("wangcai","balck")
console.log(dog1)// Dog {name: "wangcai", color: "balck"}function Animal(name,color){
this.name=name;
this.color=color;
this.live=true;
}
function Dog(){
Animal.apply(this, arguments);
}
Dog.prototype=new Animal()
var dog1=new Dog("wangcai","black")
console.log(dog1)// Dog {name: "wangcai", color: "black", live: true}Vue 同 Jquery 一样,本质也是一个大型的类库。
// 定义Vue构造函数,形参options
function Vue (options) {
if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
// ...
this._init(options)
}// 功能函数
// 引入初始化混合函数
import { initMixin } from './init'
// 引入状态混合函数
import { stateMixin } from './state'
// 引入视图渲染混合函数
import { renderMixin } from './render'
// 引入事件混合函数
import { eventsMixin } from './events'
// 引入生命周期混合函数
import { lifecycleMixin } from './lifecycle'
// 引入warn控制台错误提示函数
import { warn } from '../util/index'
...
// 挂载初始化方法
initMixin(Vue)
// 挂载状态处理相关方法
stateMixin(Vue)
// 挂载事件响应相关方法
eventsMixin(Vue)
// 挂载生命周期相关方法
lifecycleMixin(Vue)
// 挂载视图渲染方法
renderMixin(Vue)
vue中dom的更像并不是实时的,当数据改变后,vue会把渲染watcher添加到异步队列,异步执行,同步代码执行完成后再统一修改dom。
这一 part 在 Watcher 的原型链上定义了get、addDep、cleanupDeps、update、run、evaluate、depend、teardown 方法,即 Watcher 的具体实现的一些方法,比如新增依赖、清除、更新试图等。
每个Vue组件都有一个对应的watcher,这个watcher将会在组件render的时候收集组件所依赖的数据,并在依赖有更新的时候,触发组件重新渲染。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// 如果是Vue的实例,则不需要被observe
// a flag to avoid this being observed
vm._isVue = true
// merge options
// 第一步: options参数的处理
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
// mergeOptions接下来我们会详细讲哦~
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 第二步: renderProxy
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 第三步: vm的生命周期相关变量初始化
initLifecycle(vm)
// 第四步: vm的事件监听初始化
initEvents(vm)
// 第五步: vm的编译render初始化
initRender(vm)
// 第六步: vm的beforeCreate生命钩子的回调
callHook(vm, 'beforeCreate')
// 第七步: vm在data/props初始化之前要进行绑定
initInjections(vm) // resolve injections before data/props
// 第八步: vm的sate状态初始化
initState(vm)
// 第九步: vm在data/props之后要进行提供
initProvide(vm) // resolve provide after data/props
// 第十步: vm的created生命钩子的回调
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 第十一步:render & mount
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}主要是为我们的Vue原型上定义一个方法_init。然后当我们执行new Vue(options) 的时候,会调用这个方法。而这个_init方法的实现,便是我们需要关注的地方。 前面定义vm实例都挺好理解的,主要我们来看一下mergeOptions这个方法,其实Vue在实例化的过程中,会在代码运行后增加很多新的东西进去。我们把我们传入的这个对象叫options,实例中我们可以通过vm.$options访问到。

从 0 至 5000 行我们可以清晰看到 Vue 模板编译的轮廓了。
我们可以总结:Vue 的核心就是 VDOM !对 DOM 对象的操作调整为操作 VNode 对象,采用 diff 算法比较差异,一次 patch。
render 的流程是:
推荐阅读:link
// 定义 Vue 构造函数
function Vue (options) {
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
// 将 Vue 作为参数传递给导入的五个方法
initMixin(Vue);// 初始化 Mixin
stateMixin(Vue);// 状态 Mixin
eventsMixin(Vue);// 事件 Mixin
lifecycleMixin(Vue);// 生命周期 Mixin
renderMixin(Vue);// 渲染 Mixin这一部分就是初始化函数的调用。
//
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
});为什么这么写?
Object.defineProperty能保护引入的库不被重新赋值,如果你尝试重写,程序会抛出“TypeError: Cannot assign to read only property”的错误。
// 版本
Vue.version = '2.6.11';这一部分是 Vue index.js 的内容,包括 Vue 的整个挂在过程
Vue.prototype._init = function (options) {} Vue.prototype.$props
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function(){} Vue.prototype.$on
Vue.prototype.$once
Vue.prototype.$off
Vue.prototype.$emitVue.prototype._update
Vue.prototype.$forceUpdate
Vue.prototype.$destroyVue.prototype._render
Vue.prototype._o = markOnce
Vue.prototype._n = toNumber
Vue.prototype._s = toString
Vue.prototype._l = renderList
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = renderStatic
Vue.prototype._f = resolveFilter
Vue.prototype._k = checkKeyCodes
Vue.prototype._b = bindObjectProps
Vue.prototype._v = createTextVNode
Vue.prototype._e = createEmptyVNode
Vue.prototype._u = resolveScopedSlots
Vue.prototype._g = bindObjectListenersmergeOptions使用策略模式合并传入的options和Vue.options合并后的代码结构, 可以看到通过合并策略components,directives,filters继承了全局的, 这就是为什么全局注册的可以在任何地方使用,因为每个实例都继承了全局的, 所以都能找到。
推荐阅读:
new 一个 Vue 对象发生了什么:

// these are reserved for web because they are directly compiled away
// during template compilation
// 这些是为web保留的,因为它们是直接编译掉的
// 在模板编译期间这一 part 没有特别要说的,主要是对 class 的转码、合并和其他二次封装的工具函数。实际上我们在 Vue 源码很多地方看到了这样的封装,在平常的开发中,我们也得要求自己封装基本的函数。如果能形成自己习惯用的函数的库,会方便很多,且对自己能力也是一个提升。
// nodeOps:
createElement: createElement$1, //创建一个真实的dom
createElementNS: createElementNS, //创建一个真实的dom svg方式
createTextNode: createTextNode, // 创建文本节点
createComment: createComment, // 创建一个注释节点
insertBefore: insertBefore, //插入节点 在xxx dom 前面插入一个节点
removeChild: removeChild, //删除子节点
appendChild: appendChild, //添加子节点 尾部
parentNode: parentNode, //获取父亲子节点dom
nextSibling: nextSibling, //获取下一个兄弟节点
tagName: tagName, //获取dom标签名称
setTextContent: setTextContent, // //设置dom 文本
setStyleScope: setStyleScope //设置组建样式的作用域这里的重点想必就是 “ref” 了
在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作 DOM 元素。不过也确实在一些情况下做这些事情是合适的。ref 为我们提供了解决途径。
ref属性不是一个标准的HTML属性,只是Vue中的一个属性。
Virtual DOM !
没错,这里就是 虚拟 dom 生成的源码相关。
createElement方法接收一个tag参数,在内部会去判断tag标签的类型,从而去决定是创建一个普通的VNode还是一个组件类VNode;
createComponent 的实现,在渲染一个组件的时候的 3 个关键逻辑:
我们传入的 vnode 是组件渲染的 vnode,也就是我们之前说的 vm._vnode,如果组件的根节点是个普通元素,那么 vm._vnode 也是普通的 vnode,这里 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值是 false。接下来的过程就系列一的步骤一样了,先创建一个父节点占位符,然后再遍历所有子 VNode 递归调用 createElm,在遍历的过程中,如果遇到子 VNode 是一个组件的 VNode,则重复过程,这样通过一个递归的方式就可以完整地构建了整个组件树。
initComponent 初始化组建,如果没有tag标签则去更新真实dom的属性,如果有tag标签,则注册或者删除ref 然后为insertedVnodeQueue.push(vnode);
// diff 算法就在这里辣!详解link
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, elmToMove, refElm
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
if (isUndef(idxInOld)) {
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
elmToMove = oldCh[idxInOld]
if (sameVnode(elmToMove, newStartVnode)) {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}reactivateComponent 承接上文 createComponent
Vue 的核心思想:组件化。
这一部分是关于构建组件树,形成虚拟 dom ,以及非常重要的 patch 方法。
再来亿遍:
联系前后QA
Q:vue.js 同时多个赋值是一次性渲染还是多次渲染DOM?
A:官网已给出答案:cn.vuejs.org/v2/guide/re…
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
例如,当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。
这样是不是有种前后连贯起来的感觉,原来 nextTick 是这样子的。
前面围绕“指令”和“过滤器”的一些基础工具函数。
后面围绕为虚拟 dom 添加属性、事件等具体实现函数。
/*
* Parse a v-model expression into a base path and a final key segment.
* Handles both dot-path and possible square brackets.
* 将 v-model 表达式解析为基路径和最后一个键段。
* 处理点路径和可能的方括号。
*/// 如果数据是object.info.name的情况下 则返回是 {exp: "object.info",key: "name"} // 如果数据是object[info][name]的情况下 则返回是 {exp: "object[info]",key: "name"}
这一部分包括:原生指令 v-bind 和为虚拟 dom 添加 model 属性,以及格式校验工具函数。
区别:
Q:你知道v-model的原理吗?说说看
A: v-model本质上是语法糖,即利用v-model绑定数据,其实就是既绑定了数据,又添加了一个input事件监听 link
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
1. bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
2. inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
3. update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
4. componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
5. unbind:只调用一次,指令与元素解绑时调用。1. el:指令所绑定的元素,可以用来直接操作 DOM 。
2. binding:一个对象,包含以下属性:
name:指令名,不包括 v- 前缀。
value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。
modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
3. vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。
4. oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。
/**
* parent component style should be after child's
* so that parent component's style could override it
* 父组件样式应该在子组件样式之后
* 这样父组件的样式就可以覆盖它
* 循环子组件和组件的样式,把它全部合并到一个样式对象中返回 样式对象 如{width:100px,height:200px} 返回该字符串。
*/这一部分关于:对真实 dom 的操作,包括样式的增删、事件的增删、动画类等。
回过头再理一下宏观上的东西,再来亿遍-虚拟DOM:模板 → 渲染函数 → 虚拟DOM树 → 真实DOM

那么这一部分则处在“虚拟DOM树 → 真实DOM”这个阶段
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
// in a locale-dependent way, using a comma instead of a dot.
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
// as a floor function) causing unexpected behaviors
// 根据本地的依赖方式,Chromium 的旧版本(低于61.0.3163.100)格式化浮点数字,使用逗号而不是点。如果逗号未用点代替,则输入将被四舍五入而导致意外行为// activeInstance will always be the <transition> component managing this
// transition. One edge case to check is when the <transition> is placed
// as the root node of a child component. In that case we need to check
// <transition>'s parent for appear check.
// activeInstance 将一直作为<transition>的组件来管理 transition。要检查的一种边缘情况:<transition> 作为子组件的根节点时。在这种情况下,我们需要检查 <transition> 的父项的展现。更新指令 比较 oldVnode 和 vnode,根据oldVnode和vnode的情况 触发指令钩子函数bind,update,inserted,insert,componentUpdated,unbind钩子函数
此节前部分是 transition 动画相关工具函数,后部分关于虚拟 Dom patch、指令的更新。
// 定义在组件根内部递归搜索可能存在的 transition
// in case the child is also an abstract component, e.g. <keep-alive>
// we want to recursively retrieve the real component to be rendered
// 如果子对象也是抽象组件,例如<keep-alive>
// 我们要递归地检索要渲染的实际组件前部分以及此部分大部分围绕 Transition 这个关键对象。即迎合官网 “过渡 & 动画” 这一节,是我们需要关注的重点!
Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。包括以下工具:
在这里,我们只会讲到进入、离开和列表的过渡,你也可以看下一节的管理过渡状态。
vue - transition 里面大有东西,这里有一篇“细谈”推荐阅读。
// we divide the work into three loops to avoid mixing DOM reads and writes
// in each iteration - which helps prevent layout thrashing.
//我们将工作分为三个 loops,以避免将 DOM 读取和写入混合在一起
//在每次迭代中-有助于防止布局冲撞。// 安装平台运行时指令和组件
extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);Q: vue自带的内置组件有什么?
A: Vue中内置的组件有以下几种:
component组件:有两个属性---is inline-template
渲染一个‘元组件’为动态组件,按照'is'特性的值来渲染成那个组件
transition组件:为组件的载入和切换提供动画效果,具有非常强的可定制性,支持16个属性和12个事件
transition-group:作为多个元素/组件的过渡效果
keep-alive:包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们
slot:作为组件模板之中的内容分发插槽,slot元素自身将被替换
// install platform specific utils // 安装平台特定的工具
Vue.config.mustUseProp = mustUseProp;
Vue.config.isReservedTag = isReservedTag;
Vue.config.isReservedAttr = isReservedAttr;
Vue.config.getTagNamespace = getTagNamespace;
Vue.config.isUnknownElement = isUnknownElement;// public mount method
Vue.prototype.$mount = function (
el, // 真实dom 或者是 string
hydrating //新的虚拟dom vonde
) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};devtools global hook // 开发环境全局 hook Tip
parseHTML 这个函数实现大概两百多行,是一个比较大的函数体了。
parseHTML 中的方法用于处理HTML开始和结束标签。
parseHTML 方法的整体逻辑是用正则判断各种情况,进行不同的处理。其中调用到了 options 中的自定义方法。
options 中的自定义方法用于处理AST语法树,最终返回出整个AST语法树对象。
贴一下源码,有兴趣可自行感受一二。附一篇详解Vue.js HTML解析细节学习
function parseHTML(html, options) {
var stack = [];
var expectHTML = options.expectHTML;
var isUnaryTag$$1 = options.isUnaryTag || no;
var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;
var index = 0;
var last, lastTag;
while (html) {
last = html;
// 确保我们不在像脚本/样式这样的纯文本内容元素中
if (!lastTag || !isPlainTextElement(lastTag)) {
var textEnd = html.indexOf('<');
if (textEnd === 0) {
// Comment:
if (comment.test(html)) {
var commentEnd = html.indexOf('-->');
if (commentEnd >= 0) {
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3);
}
advance(commentEnd + 3);
continue
}
}
// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
if (conditionalComment.test(html)) {
var conditionalEnd = html.indexOf(']>');
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2);
continue
}
}
// Doctype:
// 匹配 html 的头文件
var doctypeMatch = html.match(doctype);
if (doctypeMatch) {
advance(doctypeMatch[0].length);
continue
}
// End tag:
var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[1], curIndex, index);
continue
}
// Start tag:
// 解析开始标记
var startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1);
}
continue
}
}
var text = (void 0),
rest = (void 0),
next = (void 0);
if (textEnd >= 0) {
rest = html.slice(textEnd);
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// < in plain text, be forgiving and treat it as text
next = rest.indexOf('<', 1);
if (next < 0) {
break
}
textEnd += next;
rest = html.slice(textEnd);
}
text = html.substring(0, textEnd);
}
if (textEnd < 0) {
text = html;
}
if (text) {
advance(text.length);
}
if (options.chars && text) {
options.chars(text, index - text.length, index);
}
} else {
// 处理是script,style,textarea
var endTagLength = 0;
var stackedTag = lastTag.toLowerCase();
var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
var rest$1 = html.replace(reStackedTag, function (all, text, endTag) {
endTagLength = endTag.length;
if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
text = text
.replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
}
if (shouldIgnoreFirstNewline(stackedTag, text)) {
text = text.slice(1);
}
if (options.chars) {
options.chars(text);
}
return ''
});
index += html.length - rest$1.length;
html = rest$1;
parseEndTag(stackedTag, index - endTagLength, index);
}
if (html === last) {
options.chars && options.chars(html);
if (!stack.length && options.warn) {
options.warn(("Mal-formatted tag at end of template: \"" + html + "\""), {
start: index + html.length
});
}
break
}
}
// Clean up any remaining tags
parseEndTag();
function advance(n) {
index += n;
html = html.substring(n);
}
function parseStartTag() {
var start = html.match(startTagOpen);
if (start) {
var match = {
tagName: start[1],
attrs: [],
start: index
};
advance(start[0].length);
var end, attr;
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
attr.start = index;
advance(attr[0].length);
attr.end = index;
match.attrs.push(attr);
}
if (end) {
match.unarySlash = end[1];
advance(end[0].length);
match.end = index;
return match
}
}
}
function handleStartTag(match) {
var tagName = match.tagName;
var unarySlash = match.unarySlash;
if (expectHTML) {
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag);
}
if (canBeLeftOpenTag$$1(tagName) && lastTag === tagName) {
parseEndTag(tagName);
}
}
var unary = isUnaryTag$$1(tagName) || !!unarySlash;
var l = match.attrs.length;
var attrs = new Array(l);
for (var i = 0; i < l; i++) {
var args = match.attrs[i];
var value = args[3] || args[4] || args[5] || '';
var shouldDecodeNewlines = tagName === 'a' && args[1] === 'href' ?
options.shouldDecodeNewlinesForHref :
options.shouldDecodeNewlines;
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines)
};
if (options.outputSourceRange) {
attrs[i].start = args.start + args[0].match(/^\s*/).length;
attrs[i].end = args.end;
}
}
if (!unary) {
stack.push({
tag: tagName,
lowerCasedTag: tagName.toLowerCase(),
attrs: attrs,
start: match.start,
end: match.end
});
lastTag = tagName;
}
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end);
}
}
function parseEndTag(tagName, start, end) {
var pos, lowerCasedTagName;
if (start == null) {
start = index;
}
if (end == null) {
end = index;
}
// Find the closest opened tag of the same type
if (tagName) {
lowerCasedTagName = tagName.toLowerCase();
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
// If no tag name is provided, clean shop
pos = 0;
}
if (pos >= 0) {
// Close all the open elements, up the stack
for (var i = stack.length - 1; i >= pos; i--) {
if (i > pos || !tagName &&
options.warn
) {
options.warn(
("tag <" + (stack[i].tag) + "> has no matching end tag."), {
start: stack[i].start,
end: stack[i].end
}
);
}
if (options.end) {
options.end(stack[i].tag, start, end);
}
}
// Remove the open elements from the stack
stack.length = pos;
lastTag = pos && stack[pos - 1].tag;
} else if (lowerCasedTagName === 'br') {
if (options.start) {
options.start(tagName, [], true, start, end);
}
} else if (lowerCasedTagName === 'p') {
if (options.start) {
options.start(tagName, [], false, start, end);
}
if (options.end) {
options.end(tagName, start, end);
}
}
}
}Regular Expressions // 相关正则
parse 函数从 9593 行至 9914 行,共三百多行。核心吗?当然核心!
引自 wikipedia:
在计算机科学和语言学中,语法分析(英语:syntactic analysis,也叫 parsing)是根据某种给定的形式文法对由单词序列(如英语单词序列)构成的输入文本进行分析并确定其语法结构的一种过程。 语法分析器(parser)通常是作为编译器或解释器的组件出现的,它的作用是进行语法检查、并构建由输入的单词组成的数据结构(一般是语法分析树、抽象语法树等层次化的数据结构)。语法分析器通常使用一个独立的词法分析器从输入字符流中分离出一个个的“单词”,并将单词流作为其输入。实际开发中,语法分析器可以手工编写,也可以使用工具(半)自动生成。
parse 的整体流程实际上就是先处理了一些传入的options,然后执行了parseHTML 函数,传入了template,options和相关钩子。
具体实现这里盗一个图:

parse中的语法分析可以看这一篇这一节
parse、optimize、codegen的核心思想解读可以看这一篇这一节
这里实现的细节还真不少!
噫嘘唏!来到第 20 篇的小结!来个图镇一下先!
还记得官方这样的一句话吗?
下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。
看了这么多,我们再回头看看注释版。

上图值得一提的是:Has "template" option? 这个逻辑的细化
碰到是否有 template 选项时,会询问是否要对 template 进行编译:即模板通过编译生成 AST,再由 AST 生成 Vue 的渲染函数,渲染函数结合数据生成 Virtual DOM 树,对 Virtual DOM 进行 diff 和 patch 后生成新的UI。
如图(此图前文也有提到,见 0 至 5000 行总结):

将 Vue 的源码的“数据监听”、“虚拟 DOM”、“Render 函数”、“组件编译”、结合好,则算是融会贯通了!
一图胜万言

好好把上面的三张图看懂,便能做到“成竹在胸”,走遍天下的 VUE 原理面试都不用慌了。框架就在这里,细化的东西就需要多多记忆了!
🙌 到 1w 行了,自我庆祝一下!
export function processElement (
element: ASTElement,
options: CompilerOptions
) {
processKey(element)
// determine whether this is a plain element after
// removing structural attributes
element.plain = (
!element.key &&
!element.scopedSlots &&
!element.attrsList.length
)
processRef(element)
processSlotContent(element)
processSlotOutlet(element)
processComponent(element)
for (let i = 0; i < transforms.length; i++) {
element = transforms[i](element, options) || element
}
processAttrs(element)
return element
}可以看到主要函数包括:processKey、processRef、processSlotContent、processSlotOutlet、processComponent、processAttrs 和最后遍历执行的transforms。
processElement完成的slotTarget的赋值,这里则是将所有的slot创建的astElement以对象的形式赋值给currentParent的scopedSlots。以便后期组件内部实例话的时候可以方便去使用vm.?slot。
先说组件内的属性摘取,主要是slot标签的name属性,这是processSlotOutLet完成的。
// handle <slot/> outlets
function processSlotOutlet (el) {
if (el.tag === 'slot') {
el.slotName = getBindingAttr(el, 'name') // 就是这一句了。
if (process.env.NODE_ENV !== 'production' && el.key) {
warn(
`\`key\` does not work on <slot> because slots are abstract outlets ` +
`and can possibly expand into multiple elements. ` +
`Use the key on a wrapping element instead.`,
getRawBindingAttr(el, 'key')
)
}
}
}
// 其次是摘取需要替换的内容,也就是 processSlotContent,这是是处理展示在组件内部的slot,但是在这个地方只是简单的将给el添加两个属性作用域插槽的slotScope和 slotTarget,也就是目标slot。这一部分仍是衔接这 parse function 里的具体实现:start、end、comment、chars四大函数。
流程再回顾一下:
一、普通标签处理流程描述
const match = { // 匹配startTag的数据结构 tagName: 'div', attrs: [ { 'id="xxx"','id','=','xxx' }, ... ], start: index, end: xxx } 复制代码 2. 处理attrs,将数组处理成 {name:'xxx',value:'xxx'} 3. 生成astElement,处理for,if和once的标签。 4. 识别结束标签,将没有闭合标签的元素一起处理。 5. 建立父子关系,最后再对astElement做所有跟Vue 属性相关对处理。slot、component等等。
二、文本或表达式的处理流程描述。
三、注释流程描述
parseHTML() 和 parse() 这两个函数占了很大的篇幅,值得重点去看看。的确也很多细节,一些正则的匹配,字符串的操作等。从宏观上把握从 template 到 vnode 的 parse 流程也无大问题。
var baseOptions = {
expectHTML: true, //标志 是html
modules: modules$1, //为虚拟dom添加staticClass,classBinding,staticStyle,styleBinding,for,
//alias,iterator1,iterator2,addRawAttr ,type ,key, ref,slotName
//或者slotScope或者slot,component或者inlineTemplate ,plain,if ,else,elseif 属性
directives: directives$1, //根据判断虚拟dom的标签类型是什么?给相应的标签绑定 相应的 v-model 双数据绑定代码函数,
//为虚拟dom添加textContent 属性,为虚拟dom添加innerHTML 属性
isPreTag: isPreTag, // 判断标签是否是 pre
isUnaryTag: isUnaryTag, // 匹配标签是否是area,base,br,col,embed,frame,hr,img,input,
// isindex,keygen, link,meta,param,source,track,wbr
mustUseProp: mustUseProp,
canBeLeftOpenTag: canBeLeftOpenTag,// 判断标签是否是 colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source
isReservedTag: isReservedTag, // 保留标签 判断是不是真的是 html 原有的标签 或者svg标签
getTagNamespace: getTagNamespace, // 判断 tag 是否是svg或者math 标签
staticKeys: genStaticKeys(modules$1) // 把数组对象 [{ staticKeys:1},{staticKeys:2},{staticKeys:3}]连接数组对象中的 staticKeys key值,连接成一个字符串 str=‘1,2,3’
};/**
* Goal of the optimizer: walk the generated template AST tree
* and detect sub-trees that are purely static, i.e. parts of
* the DOM that never needs to change.
*
* Once we detect these sub-trees, we can:
*
* 1. Hoist them into constants, so that we no longer need to
* create fresh nodes for them on each re-render;
* 2. Completely skip them in the patching process.
*/
// 优化器的目标:遍历生成的模板AST树检测纯静态的子树,即永远不需要更改的DOM。
// 一旦我们检测到这些子树,我们可以:
// 1。把它们变成常数,这样我们就不需要了
// 在每次重新渲染时为它们创建新的节点;
// 2。在修补过程中完全跳过它们。简单来说:整个 optimize 的过程实际上就干 2 件事情,markStatic(root) 标记静态节点 ,markStaticRoots(root, false) 标记静态根节点。
那么被判断为静态根节点的条件是什么?
其实,markStaticRoots()方法针对的都是普通标签节点。表达式节点与纯文本节点都不在考虑范围内。
markStatic()得出的static属性,在该方法中用上了。将每个节点都判断了一遍static属性之后,就可以更快地确定静态根节点:通过判断对应节点是否是静态节点 且 内部有子元素 且 单一子节点的元素类型不是文本类型。
只有纯文本子节点时,他是静态节点,但不是静态根节点。静态根节点是 optimize 优化的条件,没有静态根节点,说明这部分不会被优化。
Q:为什么子节点的元素类型是静态文本类型,就会给 optimize 过程加大成本呢?
A:optimize 过程中做这个静态根节点的优化目是:在 patch 过程中,减少不必要的比对过程,加速更新。但是需要以下成本
纯文本直接对比即可,不进行 optimize 将会更高效。
// KeyboardEvent.keyCode aliases
if(${condition})return null;不管是组件还是普通标签,事件处理代码都在genData的过程中,和之前分析原生事件一致,genHandlers用来处理事件对象并拼接成字符串。
// generate(ast, options)
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}export function genElement (el: ASTElement,
state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
// 如果是一个静态的树, 如 <div id="app">123</div>
// 生成_m()方法
// 静态的渲染函数被保存至staticRenderFns属性中
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
// v-once 转化为_o()方法
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
// _l()
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
// v-if 会转换为表达式
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
// 如果是template,处理子节点
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
// 如果是插槽,处理slot
return genSlot(el, state)
} else {
// component or element
let code
// 如果是组件,处理组件
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}这里面的逻辑、细节太多了,不做赘述,有兴趣了解的童鞋可以去看推荐阅读
generate方法内部逻辑还是很复杂的,但仅做了一件事情,就是将ast转化为render函数的字符串,形成一个嵌套结构的方法,模版编译生成的_c(),_m(),_l等等其实都是生成vnode的方法,在执行vue.$mount方法的时候,会调用vm._update(vm._render(), hydrating)方法,此时_render()中方法会执行生成的render()函数,执行后会生成vnode,也就是虚拟dom节点。
return function createCompiler (baseOptions) {
function compile (
template,
options
) {
var finalOptions = Object.create(baseOptions);
var errors = [];
var tips = [];
var warn = function (msg, range, tip) {
(tip ? tips : errors).push(msg);
};
if (options) {
if (options.outputSourceRange) {
// $flow-disable-line
var leadingSpaceLength = template.match(/^\s*/)[0].length;
warn = function (msg, range, tip) {
var data = { msg: msg };
if (range) {
if (range.start != null) {
data.start = range.start + leadingSpaceLength;
}
if (range.end != null) {
data.end = range.end + leadingSpaceLength;
}
}
(tip ? tips : errors).push(data);
};
}
// merge custom modules
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules);
}
// merge custom directives
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
);
}
// copy other options
for (var key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key];
}
}
}
finalOptions.warn = warn;
var compiled = baseCompile(template.trim(), finalOptions);
{
detectErrors(compiled.ast, warn);
}
compiled.errors = errors;
compiled.tips = tips;
return compiled
}再看这张图,对于“模板编译”是不是有一种新的感觉了。

// 最后的最后
return Vue;哇!历时一个月左右,我终于完成啦!!!
完结撒花🎉🎉🎉!激动 + 释然 + 感恩 + 小满足 + ...... ✿✿ヽ(°▽°)ノ✿
这生啃给我牙齿都啃酸了!!
emmm,本来打算再多修补一下,但是看到 vue3 的源码解析已有版本出来啦(扶朕起来,朕还能学),时不我待,Vue3 奥利给,干就完了!
后续仍会完善此文,您的点赞是我最大的动力!也望大家不吝赐教,不吝赞美~
最最最最后,还是那句老话,与君共勉:
纸上得来终觉浅 绝知此事要躬行