在解释vue初始化流程的时候,先回顾一下混入mixin,和插槽吧
mixin 复用
基础使用
混入提供了一种非常灵活的方式,来分发Vue组件中的可复用功能,一个混入对象可以包含任意组件选项,当组件使用混入对象时,所有混入对象的选项将被‘混合’进入该组件本身的选项
const mixin ={
created:function(){
this.hello()
},
methods:{
hello:function(){
console.log('hello from mixin')
}
}
}
export default {
name: 'App',
mixins:[mixin],
components: {
HelloWorld
}
}
选项合并
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行合并。
1. 比如,数据对象在内部会进行递归合并,并在冲突发生的时候以组件数据优先
递归合并的核心逻辑
当合并两个对象的时候,如果对象中的某个属性的值本身也是一个对象,Vue会继续对这个子对象执行合并操作(即递归),而不是简单的覆盖,这种逻辑会一直持续到所有层级的属性都被处理完毕,递归合并仅作用于双方共有的属性,对单方独有的属性直接保留
// Mixin
const myMixin = {
data() {
return {
config: {
theme: "dark",
layout: {
type: "grid",
columns: 3
}
}
};
}
};
// 组件
export default {
mixins: [myMixin],
data() {
return {
config: {
theme: "light", // 覆盖 Mixin 的 theme
layout: {
type: "flex" // 覆盖 Mixin 的 layout.type
}
}
};
}
};
合并后的data.config:
{
theme: "light", // 直接覆盖(非对象属性)
layout: { // 递归合并子对象
type: "flex", // 覆盖子属性
columns: 3 // 保留 Mixin 的子属性
}
}
2. 同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用
const mixin ={
created:function(){
this.hello()
},
methods:{
hello:function(){
console.log('hello from mixin')
}
}
}
export default {
name: 'App',
mixins:[mixin],
created:function(){
console.log('Inner created')
},
components: {
HelloWorld
}
}
此时mixin中的created钩子先被调用
3. 值为对象的选项,例如methods,components和directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对
const myMixin = {
methods: {
greet() {
console.log("Hello from Mixin!");
},
commonMethod() {
console.log("Mixin's common method");
}
}
};
export default {
mixins: [myMixin],
methods: {
commonMethod() {
console.log("Component's common method"); // 覆盖 Mixin 的同名方法
},
componentMethod() {
console.log("Component's unique method");
}
}
};
全局注册mixin
// main.js
import Vue from 'vue'
// 定义全局 Mixin
Vue.mixin({
created() {
console.log('全局 Mixin 的 created 钩子被触发');
},
methods: {
$globalMethod() {
console.log('全局方法被调用');
}
}
});
new Vue({ ... }).$mount('#app');
使用全局mixin造成的风险
- 全局 Mixin 中定义的 属性/方法名 可能与组件自身的选项或第三方 Mixin 的选项冲突。Vue 的合并策略(组件优先)可能导致意外覆盖。第三方库和vue.mixin注册顺序会影响同名方法的调用
- 若在全局 Mixin 中修改了
created
钩子逻辑,可能导致第三方组件的生命周期行为异常,引发难以追踪的 Bug。 - 全局 Mixin 的 生命周期钩子 和 响应式数据 会在所有组件中触发。若逻辑复杂或操作频繁,可能拖慢应用性能。
- 全局 Mixin 的 生命周期钩子 和 响应式数据 会在所有组件中触发。若逻辑复杂或操作频繁,可能拖慢应用性能。
- 隐式依赖:全局 Mixin 的行为对开发者不透明,新成员可能不知道某些功能来自全局 Mixin。
- 调试困难:当 Bug 由全局 Mixin 引发时,需检查所有可能受影响的组件,问题定位成本高。
- 单元测试时,全局 Mixin 的逻辑会附加到被测试组件中,导致测试环境与生产环境行为不一致,需手动模拟或清理。
- 全局 Mixin 可能直接修改组件的
data
、methods
或生命周期逻辑,破坏组件的独立性,使代码耦合度升高。
插槽slot
默认插槽:
父组件:
<template>
<div class="hello" @click="transform">
SlotDemo
<SlotComp>
<div>
我是{{ message }}父级传进来的
</div>
</SlotComp>
</div>
</template>
<script>
import SlotComp from "./SlotComp.vue";
export default {
methods: {
transform() {
this.message = this.message.toUpperCase();
},
},
data() {
return {
message: "SlotDemo",
};
},
components: {
SlotComp,
},
};
</script>
<style></style>
子组件
<template>
<div>
<slot>我是默认内容</slot>
</div>
</template>
<script>
export default {
data(){
return {}
}
}
</script>
<style></style>
具名插槽
父组件:
<div class="hello" @click="transform">
SlotDemo
<SlotComp>
<!-- 缩写 #header <template #header> -->
<template v-slot:header>
<div>我是外面传进来的头部{{ message }}</div>
</template>
<!-- 默认插槽 -->
<div>我是{{ message }}父级传进来的</div>
<!-- 相当于
<template v-slot:default>
<div>我是{{ message }}父级传进来的</div>
</template>
-->
<template v-slot:footer>
<div class=""> 我是外面传进来的尾部{{ message }}</div>
</template>
</SlotComp>
</div>
子组件:
<div>
<div class="header">
<slot name="header"></slot>
</div>
<div class="content">
<slot>我是默认内容</slot>
<!-- 如果不给name默认为default -->
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>
如果子组件在外边传递数据,可以在外边创建一个作用域名,该作用域名可使用绑定到相应具名插槽的所绑定的变量
父组件:
<template v-slot:header="header">
<div>我是外面传进来的头部{{ message }}==={{ header.msg }}==={{ JSON.stringify(header) }}</div>
</template>
子组件:
<div class="header">
<slot name="header" :msg="msg">我是默认头部</slot>
</div>
插槽实现原理:
插槽的本质就是函数!
$slots(默认插槽和具名插槽):
当子组件vm实例化时,获取到⽗组件传⼊的slot标签的内容,存放在vm.$slots中,默认插 槽为vm.$slots.default,具名插槽为vm.$slots.xxx,xxx 为插槽名,当组件执⾏渲染函数时候,遇到slot 标签,使⽤$slot中的内容进⾏替换。
$scopedSlots(作用域插槽):
在vue组件中,子组件通过$scopedSlots暴露作用域插槽函数(将内部数据封装成一个函数,并允许父组件调用此函数来获得子组件的数据),如(this.$scopedSlots.default),调用这些函数的时候传入子组件内部的数据,父组件通过v-slot接收此数据并定义插槽内容,最终,子组件将父组件基于数据动态渲染的插槽内容插入到指定位置,实现子传数据,父定结构的解耦渲染模式
vue2核心源码解析
flow
在js文件上方使用// @flow是Flow静态类型检查工具的标记,它会触发以下操作:
1.启用类型检查:Flow会扫描该文件,对变量、函数参数和返回值进行静态类型推断和验证
// @flow
function add(a: number, b: number): number { // 显式类型注解
return a + b;
}
add("1", 2); // Flow 会报错:参数类型不匹配
2.类型擦除(构建时):通过Babel插件(如@babel/preset-flow)或Flow CLI工具移除类型注解,生成纯js代码
function add(a, b) {
return a + b;
}
对比TS
相似性:都通过静态类型增强JS可靠性
区别:Flow时Facebook开发的渐进式类型工具,通过注释集成,TS式微软开发的超集语言,需要编译为JS
build.js代码解析(代码打包)
代码中所使用的包:
fs:
node.js内置模块,用于进行文件系统操作,用于读写文件
在build.js的场景:
- 读取配置文件,比如说rollup.config.js
- 将打包好的代码写入到磁盘中(比如dist目录)
- 删除旧的构建产物(如清理dist文件夹)
import fs from 'fs';
// 写入文件
fs.writeFileSync('dist/bundle.js', code);
// 读取文件
const config = fs.readFileSync('rollup.config.js', 'utf-8');
path:
nodejs内置模块,用于处理文件路径,,确保跨平台的兼容性
在build.js中的场景:
- 拼接不同操作系统的路径(如src/index.js=>dist/bundle.js)
- 解析绝对路径(如基于项目根目录定位文件)
import path from 'path';
// 拼接路径
const inputPath = path.join(__dirname, 'src', 'index.js');
// 解析绝对路径
const outputDir = path.resolve(__dirname, 'dist');
zlib:
node.js内置模块,用于压缩文件(如生成.gz)格式的静态资源
在build.js中的使用场景:
- 对构建产物(如JS/CSS文件)进行GZIP压缩,减少网络传输体积
- 生成压缩文件供服务器直接使用(如Nginx配置压缩文件)
import zlib from 'zlib';
// 压缩文件
const gzipBuffer = zlib.gzipSync(code);
fs.writeFileSync('dist/bundle.js.gz', gzipBuffer);
terser:
第三方库,用于代码压缩与混淆,优化JS文件体积
在build.js中的使用场景:
- 移除注释,空白符和调试代码(如console.log())
- 缩短变量名(如longVariableName=>a)
- 启用高级优化(如死代码(程序中存在但是永远不会被执行或使用的代码片段)删除,表达式简化)
import { minify } from 'terser';
// 压缩代码
const result = await minify(code, {
compress: true,
mangle: true,
});
fs.writeFileSync('dist/bundle.min.js', result.code);
rollup
打包工具核心库,用于模块打包,处理依赖分析与Tree-Shaking
典型场景:
- 将分散的模块打包为单个文件(如bundle.js)
- 移除未使用的代码(Tree-Shaking优化)
- 集成插件(如Babel转译,CSS处理)
import rollup from 'rollup';
// 使用 Rollup API 打包
const bundle = await rollup.rollup({
input: 'src/index.js',
plugins: [ /* ... */ ],
});
await bundle.write({
file: 'dist/bundle.js',
format: 'esm',
});
上述几个包完整的协作流程
- 路径处理(path):确定输入文件(src/index.js)和输出目录(dist)
- 模块打包(rollup):分析依赖,打包代码,生成未优化的bundle.js
- 代码压缩(terser):压缩打包后的代码,生成bundle.min.js
- 文件压缩(zlib):对bundle.min.js进行GZIP压缩,生成bundle.ming.js.gz
- 文件写入(fs):将最终产物写入磁盘,完成构建
build.js的工作流程:
首先将build配置对象通过getAllBuilds(config.js文件导出的方法)方法将build处理为rollup能够解析的配置builds,将处理后的builds进行过滤,如果在输入npm run build后面还有其他参数(filters)的话,过滤出来builds中包括传入的参数的部分,如果没有的话默认过滤掉weex,其他的全部保留,之后进行rollup的编译过程(build(builds))
打包核心代码:
runtime only和runtime + complier
Vue.js 中,Runtime Only 和 Runtime + Compiler 是两种不同的构建版本,主要区别在于是否包含 模板编译器(Template Compiler)。
1. Runtime Only(仅运行时)
包含内容:
- Vue 的核心功能(响应式系统、虚拟 DOM、组件生命周期等)。
- 不包含模板编译器。
适用场景:
- 使用
.vue
单文件组件(通过vue-loader
或@vitejs/plugin-vue
预编译模板)。 - 在构建阶段(如 Webpack、Vite)提前将模板编译为 Render 函数。
- 使用
代码示例:
// 必须使用 Render 函数(不能直接写 template 字符串) new Vue({ render(h) { return h('div', 'Hello, World!'); } }).$mount('#app');
优点:
- 体积更小(比 Runtime + Compiler 小约 30%)。
- 性能更高(避免运行时编译模板的开销)。
2. Runtime + Compiler(运行时 + 编译器)
包含内容:
- Vue 的核心功能。
- 模板编译器(可在运行时将模板字符串编译为 Render 函数)。
适用场景:
- 直接在 JavaScript 中写
template
字符串(未预编译)。 - 在 HTML 中直接使用
template
或 DOM 模板(如<div id="app">{{ message }}</div>
)。 - 未使用构建工具(如直接通过
<script>
引入 Vue)。
- 直接在 JavaScript 中写
代码示例:
// 可以直接使用 template 字符串 new Vue({ template: '<div>{{ message }}</div>', data() { return { message: 'Hello, World!' }; } }).$mount('#app');
缺点:
- 体积更大(包含编译器代码)。
- 运行时编译模板会带来性能损耗(尤其是复杂模板)。
核心对比表
特性 | Runtime Only | Runtime + Compiler |
---|---|---|
体积 | 更小(~30KB) | 更大(~40KB) |
模板编译时机 | 构建阶段(预编译) | 运行时(动态编译) |
使用方式 | 必须预编译模板或使用 Render 函数 | 可直接写模板字符串或 DOM 模板 |
适用场景 | 现代前端工程化项目(Webpack/Vite) | 传统 HTML 项目或动态模板需求 |
如何选择?
推荐使用 Runtime Only:
绝大多数现代项目(如 Vue CLI 或 Vite 创建的项目)默认使用 Runtime Only,因为它更高效且体积更小。必须使用 Runtime + Compiler:
以下情况需要选择包含编译器的版本:- 直接通过
<script>
标签引入 Vue(未使用构建工具)。 - 需要动态编译用户输入的模板字符串(如在线代码编辑器)。
- 直接通过
默认情况下,Vue CIL生成的项目使用Runtime Only,如果需要切换成Runtime+Compiler,需要修改vue.config.js:
// vue.config.js
module.exports = {
runtimeCompiler: true // 启用 Runtime + Compiler
};
Vue被设计为一个构造函数原因
在Vue.js中,Vue被设计为一个构造函数而不是ES6的class,主要原因与设计模式,灵活性和运行时代码结构有关
1. 构造函数模式的优势
Vue 2.x 的源码采用 构造函数 + 原型链继承 的传统模式(而非 ES6 的 class
),核心原因包括:
a. 更灵活的实例化控制
- 构造函数 允许在实例化时动态调整行为(例如合并全局配置、处理组件选项)。
- 示例:在
new Vue(options)
时,构造函数内部会调用this._init(options)
,进行选项合并、生命周期初始化等复杂操作:function Vue(options) { if (!(this instanceof Vue)) { // 防止直接调用(必须用 new) warn('Vue is a constructor and should be called with the `new` keyword'); } this._init(options); // 核心初始化逻辑 }
b. 原型方法的扩展性
- 通过
Vue.prototype
添加方法(如$mount
、$watch
)更直观,兼容性更好(尤其是针对旧浏览器)。 - 示例:
Vue.prototype.$mount = function (el) { // 挂载逻辑... };
2. Runtime + Compiler 的特殊处理
在 Runtime + Compiler 版本中,Vue 需要包含 模板编译器,这影响了代码结构设计:
a. 动态编译器的挂载
- 编译器代码(如
compileToFunctions
)仅在 Runtime + Compiler 版本中存在,需通过构造函数逻辑动态挂载。 - 代码示例:
// runtime + compiler 版本中,挂载编译器相关的原型方法 Vue.prototype.$compile = function (template) { // 编译模板为 render 函数 const { render } = compileToFunctions(template, { /* ... */ }); return render; };
b. 体积优化
- 使用构造函数模式,可以更灵活地通过构建工具 条件编译(如 Webpack 的
DefinePlugin
),将编译器代码仅包含在 Runtime + Compiler 版本中:if (process.env.RUNTIME_COMPILER) { // 构建时条件判断 Vue.prototype.$compile = function() { /* ... */ }; }
3. 与 ES6 Class 的对比
若使用 ES6 class
,会遇到以下限制:
ES6 Class 的限制 | 构造函数模式的解决方案 |
---|---|
无法直接实现 私有属性和方法 | 通过闭包或命名约定(如 _ 前缀)实现 |
继承和混入机制不够灵活 | 通过 Vue.extend 和 Vue.mixin 实现 |
静态方法需通过 static 定义 |
直接挂载到构造函数(如 Vue.use ) |
4. 设计模式的选择
Vue 的构造函数模式更接近 工厂模式,通过封装内部逻辑,对外暴露简洁的 API:
// 用户只需关注选项对象,无需了解内部初始化细节
new Vue({
data: { message: 'Hello' },
template: '<div>{{ message }}</div>'
});
总结
- 构造函数模式 提供了对实例化过程的完全控制,适合 Vue 的复杂初始化逻辑。
- Runtime + Compiler 版本需要动态挂载编译器代码,构造函数模式更易于条件编译和体积优化。
- ES6 Class 在灵活性和兼容性上不如传统构造函数,尤其在 Vue 2.x 的设计背景下。
在 Vue 3 中,虽然源码改用 TypeScript(使用 class
语法),但通过 Proxy 响应式系统 和 Composition API 重构,依然保持了类似的外部 API 设计思想。
构造函数和ES6class类比较
一、构造函数(Constructor Function)
1. 基本语法
通过 function
关键字定义,使用 new
关键字实例化对象:
function Person(name) {
this.name = name; // 实例属性
}
Person.prototype.sayHello = function() { // 原型方法
console.log(`Hello, ${this.name}!`);
};
const alice = new Person("Alice");
alice.sayHello(); // 输出 "Hello, Alice!"
2. 核心特点
- 原型链继承:通过
prototype
属性共享方法。 - 动态性:可在运行时动态修改原型或实例。
- 灵活性:没有语法限制,可自由操作
this
和原型。 - 兼容性:支持所有浏览器(包括旧版本)。
3. 缺点
- 语法冗长:方法需显式绑定到原型。
- 容易出错:忘记
new
会导致this
指向全局对象(如window
)。 - 不够直观:对传统面向对象开发者不够友好。
二、Class 类(ES6 语法糖)
1. 基本语法
通过 class
关键字定义,更接近传统面向对象语言:
class Person {
constructor(name) {
this.name = name; // 实例属性
}
sayHello() { // 原型方法
console.log(`Hello, ${this.name}!`);
}
static createAnonymous() { // 静态方法
return new Person("Anonymous");
}
}
const bob = new Person("Bob");
bob.sayHello(); // 输出 "Hello, Bob!"
const anon = Person.createAnonymous(); // 调用静态方法
2. 核心特点
- 语法简洁:方法直接定义在类内部,无需操作
prototype
。 - 继承简化:通过
extends
和super
实现继承。 - 内置约束:
- 必须使用
new
调用,否则报错。 - 方法默认不可枚举(
enumerable: false
)。
- 必须使用
- 现代性:更适合大型项目或团队协作。
3. 缺点
- 本质仍是原型链:底层实现与构造函数相同,只是语法糖。
- 兼容性:旧版浏览器(如 IE11)需 Babel 转译。
- 灵活性受限:无法直接操作原型链(需通过
Object.getPrototypeOf
)。
三、关键区别对比
特性 | 构造函数 | Class 类 |
---|---|---|
语法 | function + prototype |
class + constructor + 方法简写 |
方法定义 | 显式添加到 prototype |
直接写在类内部 |
继承 | 手动设置原型链(如 Parent.call(this) ) |
使用 extends 和 super |
静态方法 | 通过构造函数属性添加(如 Person.create ) |
使用 static 关键字 |
调用限制 | 忘记 new 会导致 this 指向错误 |
必须用 new ,否则抛出错误 |
方法可枚举性 | 默认可枚举(需手动设置 enumerable: false ) |
默认不可枚举 |
私有字段(ES2022) | 无法原生实现 | 支持 # 前缀的私有字段(如 #secret ) |
提升(Hoisting) | 函数声明会提升 | 类声明不会提升(必须先定义后使用) |
四、示例对比:实现继承
构造函数实现继承
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a noise.`);
};
function Dog(name) {
Animal.call(this, name); // 调用父类构造函数,在子类实例的上下文中执行父类构造函数,从而继承父类定义的实例属性(如 this.name)。
}
Dog.prototype = Object.create(Animal.prototype); //继承父类原型方法
Dog.prototype.constructor = Dog; // 修复构造函数指向
Dog.prototype.bark = function() {
console.log(`${this.name} barks!`);
};
const dog = new Dog("Rex");
dog.bark(); // 输出 "Rex barks!"
上述为组合继承:构造函数继承+原型链继承
优势:实例属性独立,避免共享,方法复用,减少内存占用(方法定义在原型上)
在 JavaScript 中,如果子类继承父类的原型后,父类原型新增了方法,子类的实例仍然可以访问到父类新增的方法。这是因为子类通过 原型链(Prototype Chain) 继承父类的方法,而原型链是基于 动态引用 的,而非静态复制。
原型链的动态性
- 继承的本质:子类的原型对象(如
Dog.prototype
)通过Object.create(Animal.prototype)
创建,其原型(__proto__
)指向父类的原型(Animal.prototype
)。- 动态查找:当访问子类实例的方法或属性时,JavaScript 会沿着原型链 实时查找,而非在继承时复制父类原型的全部内容。
Class 类实现继承
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name) {
super(name); // 必须调用 super()
}
bark() {
console.log(`${this.name} barks!`);
}
}
const dog = new Dog("Rex");
dog.bark(); // 输出 "Rex barks!"
五、使用场景
构造函数:
- 需要兼容旧浏览器(如 IE11)。
- 需要动态修改原型链或实现高度灵活的对象模式。
Class 类:
- 现代浏览器或 Node.js 环境。
- 需要清晰、结构化的面向对象代码。
- 团队协作或大型项目维护。
总结
- 构造函数 是 JavaScript 早期的对象创建方式,灵活但语法繁琐。
- Class 类 是 ES6 的语法糖,简化了原型链操作,更适合现代开发。
- 选择依据:根据项目需求、团队习惯和运行环境决定使用哪种方式。
new Vue()的时候做了什么事情
在vue2中,有
<div id="app">
{{message}}
</div>
let app =new Vue({
el:'#app',
data:{
message:'Hello Vue!'
}
})
在new Vue的过程中,进行了初始化,调用_init方法,该方法定义在vue的原型上,该方法接受一个options参数,_init方法中第一步是进行配置项的合并,该参数就是new Vue()中传入的对象,_init方法会先判断options是不是一个组件,如果是一个组件,则初始化组件,不是的话则融入options(mergeOptions())。
第二步,进行各种初始化,比如
initLifecycle(vm):初始化组件实例的生命周期相关属性,建立父子组件关系
具体行为:
- 设置
$parent
(父组件实例)、$root
(根组件实例)、$children
(子组件列表)。 - 初始化生命周期状态标志,如
_isMounted
(是否已挂载)、_isDestroyed
(是否已销毁)等。
initEvents(vm):处理父组件传递的自定义事件
具体行为:
- 初始化
_events
对象,存储事件监听器。 - 将父组件通过模板传递的事件(如
@my-event="handler"
)注册到当前组件实例。
initRender(vm):初始化与渲染相关的属性和方法
具体行为:
- 定义插槽处理逻辑,初始化
$slots
和$scopedSlots
。 - 声明实例的
$createElement
方法(用于生成虚拟 DOM)。 - 为响应式更新做准备(如
$attrs
和$listeners
的响应式处理)。
初始化这些之后,调用callHook(vm,'beforeCreate'),即执行beforeCreate()里的回调函数,此时数据观测(data),事件(methods)等尚未被初始化,仅完成基础结构(如父子关系,渲染)的初始化
接着初始化
initInjections(vm):注入父组件通过provide提供的数据
具体行为:
- 解析
inject
选项,从父级链中找到匹配的provide
值。 - 将注入的数据设置为响应式(通过
defineReactive
),并挂载到当前实例。
initState():初始化响应式状态(数据,计算属性等)
具体行为:
initProps
:解析props
,将其定义为响应式属性。-
initMethods
:将methods
绑定到实例。 -
initData
:将data
转换为响应式对象,并代理到实例。 -
initComputed
:初始化计算属性,建立依赖追踪。 -
initWatch
:设置watch
监听器。
initProvide(vm):设置当前组件提供给子孙组件的数据
具体行为:
- 解析
provide
选项(可能是函数或对象),将其挂载到实例的_provided
属性。 - 注意:
provide
在initState
之后执行,因此可以访问data
、props
等状态。
接着callHook(vm,'created'),调用created()里的回调函数,此时已完成所有状态(data,props,methods等)的初始化,但尚未挂载DOM,可在此阶段访问数据,调用方法,但无法操作DOM
第三步,初始化完成之后,会将传入的el和vm实例通过$mount进行绑定,并渲染到DOM上
$mount在vue的入口文件上就已经定义过(运行时和编译时)
src/platforms/web/entry-runtime-with-compiler.js:
// 保存原始的 $mount 方法(运行时版本的 mount,不带编译功能) const mount = Vue.prototype.$mount; // 重写 $mount 方法,添加模板编译逻辑 Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { // 1. 解析 el 选项,获取 DOM 元素 el = el && query(el); // 2. 检查 el 是否是 body 或 html 标签(生产环境警告) if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ); return this; } const options = this.$options; // 3. 处理 template/el 转换为 render 函数 if (!options.render) { let template = options.template; if (template) { // 处理 template 选项(可能是字符串、DOM 元素或选择器) if (typeof template === 'string') { if (template.charAt(0) === '#') { // 通过 ID 选择器获取模板内容 template = idToTemplate(template); } } else if (template.nodeType) { // 直接传入 DOM 元素,取其 innerHTML template = template.innerHTML; } else { // 无效模板警告 return this; } } else if (el) { // 没有 template,使用 el 的 outerHTML 作为模板 template = getOuterHTML(el); } // 4. 编译模板生成 render 函数 if (template) { // vue模板编译器的核心方法,将模板字符串转换为可执行的render函数 const { render, staticRenderFns } = compileToFunctions( template, { outputSourceRange: process.env.NODE_ENV !== 'production', shouldDecodeNewlines: shouldDecodeNewlines, shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this ); options.render = render; options.staticRenderFns = staticRenderFns; } } // 5. 调用原始 mount 方法(执行挂载) return mount.call(this, el, hydrating); };
原始的mount函数中先将vm.$options.render变成空节点,之后进入beforeMount的回调函数中, 创建渲染watcher后,执行mounted回调
第一次定义mount:src/platforms/web/runtime/index.js
// 定义基础 $mount 方法(运行时版本,不带模板编译器) Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { // 1. 解析 el 参数(仅在浏览器环境下处理) el = el && inBrowser ? query(el) : undefined; // 2. 调用核心挂载方法 mountComponent return mountComponent(this, el, hydrating); };
mountComponent
是挂载的核心方法(位于src/core/instance/lifecycle.js
),负责:
- 创建组件的渲染 Watcher。
- 触发首次渲染(通过
vm._update(vm._render(), hydrating)
)。- 处理服务端渲染(SSR)的激活逻辑(
hydrating
参数)。mountComponent:
export function mountComponent( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el; // 将 el 挂载到实例上 // 1. 若未定义 render 函数,创建空 VNode(避免未编译模板报错) if (!vm.$options.render) { vm.$options.render = createEmptyVNode; } // 2. 触发 beforeMount 生命周期钩子 callHook(vm, 'beforeMount'); // 3. 定义更新函数(渲染 + 更新 DOM) const updateComponent = () => { vm._update(vm._render(), hydrating); }; // 4. 创建渲染 Watcher,关联数据变化与视图更新 new Watcher( vm, updateComponent, noop, // 空回调(用于处理异步更新) { before: () => { /* 更新前钩子 */ } }, true // 标记为渲染 Watcher ); // 5. 触发 mounted 钩子(首次渲染完成后) hydrating = false; if (vm.$vnode == null) { vm._isMounted = true; callHook(vm, 'mounted'); } return vm; }
Vue构造函数定义完成后立即调用的方法:
在vue2源码中,Vue的构造函数首先被定义,随后立即调用各个Mixin方法,像原型添加功能,这些Mixin函数在Vue库加载时一次性执行,并不是在new Vue()实例化时调用
_initMixin(Vue):在vue的原型上添加了_init方法,Vue实例化的核心逻辑入口
Vue.prototype._init = function (options) {
const vm = this;
// 合并配置项(如 mixins、extends 等)
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
// 初始化生命周期、事件、渲染等模块
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // 依赖注入
initState(vm); // 初始化 data/props/computed/watch
initProvide(vm); // 提供数据给子组件
callHook(vm, 'created');
// 若存在 el 选项,自动调用 $mount 挂载
if (vm.$options.el) vm.$mount(vm.$options.el);
};
_init
是 Vue 实例化的核心方法,处理选项合并、生命周期钩子调用、响应式数据初始化等。- 在
new Vue()
时,构造函数内部调用this._init(options)
启动初始化。
stateMixin(Vue):为 Vue 实例添加状态管理相关的方法和属性(如 $data
、$props
、$set
、$delete
等)
function stateMixin(Vue) {
// 定义 $data 和 $props 的访问器属性(只读)
Object.defineProperty(Vue.prototype, '$data', {
get() { return this._data; },
set(newData) { /* 禁止直接修改 */ }
});
Object.defineProperty(Vue.prototype, '$props', {
get() { return this._props; },
set(newProps) { /* 禁止直接修改 */ }
});
// 添加响应式方法
Vue.prototype.$set = set; // 动态添加响应式属性
Vue.prototype.$delete = del; // 动态删除属性并触发更新
// 添加 $watch 方法
Vue.prototype.$watch = function (expOrFn, cb, options) { /* ... */ };
}
- 提供对响应式数据的操作接口(如
$set
和$delete
),解决 Vue 无法自动检测对象属性新增或删除的问题。 $data
和$props
被定义为只读属性,确保用户无法直接替换根数据。$watch
方法用于监听数据变化,是 Vue 响应式系统的核心实现之一。
lifecycleMixin(Vue):为 Vue 实例添加生命周期相关的核心方法(如 _update
、$forceUpdate
、$destroy
)。
function lifecycleMixin(Vue) {
// 负责虚拟 DOM 的更新和渲染
Vue.prototype._update = function (vnode, hydrating) {
const vm = this;
// 通过 patch 算法对比新旧 VNode,更新真实 DOM
if (!vm._isMounted) {
// 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating);
} else {
// 后续更新
vm.$el = vm.__patch__(prevVnode, vnode);
}
};
// 强制重新渲染(跳过优化)
Vue.prototype.$forceUpdate = function () {
if (this._watcher) this._watcher.update();
};
// 销毁实例
Vue.prototype.$destroy = function () {
const vm = this;
if (vm._isBeingDestroyed) return;
callHook(vm, 'beforeDestroy');
vm._isBeingDestroyed = true;
// 移除所有子组件、解绑指令、销毁 Watcher
if (vm._watcher) vm._watcher.teardown();
let i = vm._watchers.length;
while (i--) vm._watchers[i].teardown();
vm._data.__ob__?.vmCount--;
vm._isDestroyed = true;
// 触发 DOM 卸载
vm.__patch__(vm._vnode, null);
callHook(vm, 'destroyed');
};
}
_update
是渲染核心方法,负责将虚拟 DOM(VNode)转换为真实 DOM,并在数据变化时触发更新。$forceUpdate
强制组件重新渲染,常用于手动控制更新场景。$destroy
销毁组件实例,解绑所有依赖和事件监听器。
renderMixin(Vue):为 Vue 实例添加渲染相关的辅助方法(如 $nextTick
、_render
、_o
、_n
等)。
function renderMixin(Vue) {
// 安装渲染辅助函数(如 _c、_v、_s 等)
installRenderHelpers(Vue.prototype);
// 定义 $nextTick
Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this);
};
// 生成虚拟 DOM(VNode)
Vue.prototype._render = function () {
const vm = this;
const { render, _parentVnode } = vm.$options;
// 执行用户定义的 render 函数,生成 VNode
let vnode;
try {
vnode = render.call(vm._renderProxy, vm.$createElement);
} catch (e) {
handleRenderError(e, vm);
}
return vnode;
};
}
mountComponent函数中更新的render方法(renderMixin(Vue)方法中)
vm._render()的作用:
生成虚拟 DOM
调用用户定义的render
函数(或编译模板生成的render
函数),返回一个 VNode 树,描述当前组件的 UI 结构。触发响应式依赖收集
在render
函数执行过程中,访问的 data、props、computed 等响应式数据会被当前渲染 Watcher 收集为依赖,确保数据变化时触发重新渲染。处理组件、插槽、指令等高级特性
解析组件嵌套、插槽内容、自定义指令等,生成完整的 VNode 结构。
Vue.prototype._render = function (): VNode {
const vm: Component = this;
// 1. 获取 render 函数和父级 VNode(用于插槽处理)
const { render, _parentVnode } = vm.$options;
// 2. 设置父级插槽作用域(用于作用域插槽)
if (_parentVnode) {
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject;
}
// 3. 设置渲染代理(开发环境校验)
vm.$vnode = _parentVnode; // 当前组件的占位符 VNode
let vnode;
try {
// 4. 执行 render 函数,生成子 VNode 树
vnode = render.call(
vm._renderProxy, // 代理对象(开发环境校验未定义的属性)
vm.$createElement // 用户 render 函数的 createElement 参数
);
} catch (e) {
handleError(e, vm, `render`);
// 返回错误占位 VNode
vnode = vm._vnode;
}
// 5. 确保返回的 VNode 是单个根节点
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0];
}
if (!(vnode instanceof VNode)) {
// 无效 VNode 时创建空注释节点
vnode = createEmptyVNode();
}
// 6. 设置当前组件树的根 VNode
vnode.parent = _parentVnode;
return vnode;
};
总结来说,Vue.js的初始化以及渲染流程如下:
从 new Vue()
触发实例化开始,首先通过 _init
方法进行初始化:
初始化阶段(init)
- 合并配置项,建立父子组件关系(
initLifecycle
) - 注册父组件传递的事件(
initEvents
) - 处理插槽与渲染方法(
initRender
),并调用beforeCreate
钩子 - 注入依赖(
initInjections
)、初始化响应式数据状态(initState
,含 data/props/computed/watch)、通过initProvide
为子组件提供数据并触发created
钩子
- 合并配置项,建立父子组件关系(
挂载阶段($mount)
- 判断是否需要进行模板编译(图片中为
$mpile
,应为compile
),当用户未提供预编译的render
函数时,将模板(template
或el.outerHTML
)编译为render
函数 - 进入
mountComponent
,创建渲染 Watcher,绑定更新逻辑
- 判断是否需要进行模板编译(图片中为
渲染流程
- 执行
_render
方法,调用render
函数生成 VNode 虚拟节点树(vnode),期间收集响应式数据的依赖 - 通过
_update
方法进行 patch
操作,对比新旧 VNode(核心 Diff 算法),高效更新真实 DOM
- 执行
整个流程体现了 Vue 响应式驱动的核心机制:初始化建立数据与视图的依赖关系,通过 render→vnode→patch
链式更新,最终以最小的 DOM 操作实现视图的精准同步。