Vue2初始化流程以及渲染过程

发布于:2025-04-19 ⋅ 阅读:(20) ⋅ 点赞:(0)

在解释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 可能直接修改组件的 datamethods 或生命周期逻辑,破坏组件的独立性,使代码耦合度升高。

插槽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',
});

上述几个包完整的协作流程

  1. 路径处理(path):确定输入文件(src/index.js)和输出目录(dist)
  2. 模块打包(rollup):分析依赖,打包代码,生成未优化的bundle.js
  3. 代码压缩(terser):压缩打包后的代码,生成bundle.min.js
  4. 文件压缩(zlib):对bundle.min.js进行GZIP压缩,生成bundle.ming.js.gz
  5. 文件写入(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)。
  • ​代码示例​​:

    // 可以直接使用 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():初始化响应式状态(数据,计算属性等)

具体行为:

  1. initProps​:解析 props,将其定义为响应式属性。
  2. initMethods​:将 methods 绑定到实例。
  3. initData​:将 data 转换为响应式对象,并代理到实例。
  4. initComputed​:初始化计算属性,建立依赖追踪。
  5. initWatch​:设置 watch 监听器。

initProvide(vm):设置当前组件提供给子孙组件的数据

具体行为:

  • 解析 provide 选项(可能是函数或对象),将其挂载到实例的 _provided 属性。
  • ​注意​​:provide 在 initState 之后执行,因此可以访问 dataprops 等状态。

接着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()的作用:
  1. 生成虚拟 DOM​
    调用用户定义的 render 函数(或编译模板生成的 render 函数),返回一个 ​​VNode 树​​,描述当前组件的 UI 结构。

  2. ​触发响应式依赖收集​
    在 render 函数执行过程中,访问的 ​​data、props、computed​​ 等响应式数据会被当前渲染 Watcher 收集为依赖,确保数据变化时触发重新渲染。

  3. ​处理组件、插槽、指令等高级特性​
    解析组件嵌套、插槽内容、自定义指令等,生成完整的 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​ 方法进行初始化:

  1. ​初始化阶段(init)​

    • 合并配置项,建立父子组件关系(initLifecycle
    • 注册父组件传递的事件(initEvents
    • 处理插槽与渲染方法(initRender),并调用 beforeCreate 钩子
    • 注入依赖(initInjections)、初始化响应式数据状态(initState,含 data/props/computed/watch)、通过 initProvide 为子组件提供数据并触发 created 钩子
  2. ​挂载阶段($mount)​

    • 判断是否需要进行模板编译(图片中为 $mpile,应为 compile),当用户未提供预编译的 render 函数时,将模板(template 或 el.outerHTML)编译为 render 函数
    • 进入 ​mountComponent​,创建渲染 Watcher,绑定更新逻辑
  3. ​渲染流程​

    • 执行 ​_render​ 方法,调用 render 函数生成 ​​VNode 虚拟节点树(vnode)​​,期间收集响应式数据的依赖
    • 通过 ​_update​ 方法进行 ​patch​ 操作,对比新旧 VNode(核心 Diff 算法),高效更新真实 ​​DOM​

整个流程体现了 Vue ​​响应式驱动​​的核心机制:初始化建立数据与视图的依赖关系,通过 render→vnode→patch 链式更新,最终以最小的 DOM 操作实现视图的精准同步。


网站公告

今日签到

点亮在社区的每一天
去签到