跳到主要内容

Vue

Vue.js 是一款流行的前端 JavaScript 框架,用于构建用户界面和单页面应用。它的核心思想是通过数据驱动视图,将页面抽象成一个组件树,每个组件都包含了自己的 HTML 模板、JavaScript 代码和样式。

Vue 组成

  1. 响应式数据:
  • Vue.js 提供了响应式的数据绑定机制,当数据发生变化时,相关的视图会自动更新。这使得开发者无需手动操作 DOM,只需关注数据的变化,Vue.js 就会帮助你更新页面。
  1. 组件化开发:
  • Vue.js 支持组件化开发,将页面划分为多个独立的组件,每个组件包含自己的模板、脚本和样式。组件可以复用、嵌套和组合,使得代码更加模块化和可维护。
  1. 指令和事件:
  • Vue.js 提供了丰富的指令和事件,用于处理用户交互和页面逻辑。例如 v-bind 用于绑定数据到 HTML 属性、v-on 用于监听事件、v-if 和 v-for 用于条件渲染和列表渲染等。
  1. 生命周期钩子:
  • Vue.js 组件具有生命周期钩子函数,可以在组件的不同阶段执行相应的逻辑。常用的生命周期钩子包括 created、mounted、updated 和 destroyed 等。
  1. Vue Router:
  • Vue.js 提供了官方的路由库 Vue Router,用于实现单页面应用的路由管理。Vue Router 支持路由嵌套、路由参数、路由导航守卫等功能,使得页面的路由管理变得简单而灵活。
  1. Vuex:
  • Vuex 是 Vue.js 的状态管理库,用于集中管理应用的状态。Vuex 实现了一个单一的全局状态树,并提供了一些 API 和规则保证状态的改变只能通过提交 mutation 来进行,使得状态变化可追踪和可调试。
  1. 服务端渲染:
  • Vue.js 支持服务端渲染(SSR),可以在服务器端预先渲染出页面的 HTML 内容,然后将其发送给客户端,提高页面的加载性能和 SEO。

Vue 执行过程

Alt text

  1. 初始化阶段:
  • 创建 Vue 实例,进行初始化参数的合并和设置。
    • Vue 的入口文件会导出一个 Vue 构造函数,当我们使用 new Vue() 创建实例时,实际上就是调用了这个构造函数。在构造函数中,会初始化实例的一些属性,并调用 _init() 方法进行初始化。
    • 初始化参数:在 _init() 方法中,Vue 会对传入的选项进行合并处理,包括 data、methods、computed、watch 等选项。
  • 初始化生命周期钩子函数,并执行 beforeCreate 生命周期钩子。
    • 调用 initLifecycle()、initEvents()、initRender() 等方法进行生命周期、事件、渲染等方面的初始化。
  • 初始化数据响应式,包括对数据进行劫持和监听,建立数据与视图的关联。
    • 建立数据响应式:在 initState() 方法中,Vue 会对 data 数据进行处理,调用 observe() 方法将数据转换为响应式对象。在 observe() 方法中,会递归地遍历对象的属性,为每个属性创建 Dep 对象,并使用 Object.defineProperty() 或 Proxy 对象进行属性劫持,以实现数据的监听和响应。
  • 初始化事件监听器,包括对组件内部事件的绑定和处理。
    • 在初始化过程中,还会对计算属性、侦听器、组件、指令等进行初始化和注册,以及处理组件的子组件、插槽等。
// Vue.js 入口文件

// 导出 Vue 构造函数
export default class Vue {
constructor(options) {
// 调用 _init() 方法进行初始化
this._init(options);
}

// 初始化方法
_init(options) {
// 合并选项
options = mergeOptions(this.constructor.options, options || {});

// 初始化生命周期、事件等
initLifecycle(vm);
initEvents(vm);
initRender(vm);

// 初始化数据响应式
initState(vm);

// 其他初始化操作...
}
}
  1. 编译阶段:
  • 编译模板,将模板字符串编译成渲染函数。
    • 解析模板:Vue 使用正则表达式等方法解析模板字符串,识别其中的标签、指令、插值表达式等,并生成 AST(抽象语法树)。
    • 静态节点标记:Vue 编译器会遍历 AST,标记其中的静态节点(即不包含动态绑定的节点),这样在后续的渲染过程中可以跳过对这些节点的更新,提高渲染性能。
  • 解析模板中的指令和事件,生成对应的渲染函数代码。
    • 指令和事件处理器解析:编译器会解析模板中的指令(如 v-if、v-for、v-bind 等)和事件处理器(如 @click、@change 等),生成对应的渲染函数代码。
    • 动态节点生成:编译器会根据模板中的动态绑定信息,生成动态节点的渲染函数代码,包括对数据的访问和更新。
  • 优化渲染函数,生成 VNode 节点树。
    • 优化:在生成渲染函数代码的过程中,编译器会进行一些优化操作,如静态节点的提取、事件的处理器缓存等,以提高渲染性能。
// Vue 模板编译器入口

// 编译模板成渲染函数
function compile(template) {
// 解析模板生成 AST
const ast = parse(template);

// 静态节点标记
markStatic(ast);

// 指令和事件处理器解析
optimize(ast);

// 动态节点生成
const code = generate(ast);

return {
render: createFunction(code)
};
}
  1. 挂载阶段:
  • 执行 beforeMount 生命周期钩子。
    • beforeMount 钩子:在挂载开始之前被调用,相关的 render 函数首次被调用。
    • 在这个阶段,实例已经完成了数据观测等配置,但是真实 DOM 尚未生成。
  • 将 VNode 节点树渲染成真实的 DOM 元素。
    • 创建虚拟 DOM:Vue 会根据模板生成虚拟 DOM 树,这个过程通常是在编译阶段完成的,将模板编译成渲染函数,然后执行渲染函数得到虚拟 DOM。
    • 初始化渲染:Vue 首次挂载时,会执行初始化渲染函数,将虚拟 DOM 渲染成真实 DOM,并将其挂载到指定的 DOM 节点上。
  • 将渲染后的 DOM 元素挂载到页面上。
    • 生成真实 DOM:在初始化渲染的过程中,Vue 会遍历虚拟 DOM 树,根据节点类型创建对应的真实 DOM 元素,并将其插入到指定的 DOM 节点上。
    • Vue 的 _update 方法负责执行初始化渲染,其内部会调用 patch 方法来对比新旧虚拟 DOM,最终生成真实 DOM 并挂载到页面上。
  • 执行 mounted 生命周期钩子。
    • mounted 钩子:挂载完成后被调用,这时候实例已经被挂载到页面上,真实 DOM 已经生成并挂载到指定的 DOM 节点上。
// Vue 实例挂载方法
function mount(vm, el) {
// 执行 beforeMount 钩子
callHook(vm, 'beforeMount');

// 创建虚拟 DOM
vm._update(vm._render());

// 真实 DOM 挂载
el.appendChild(vm.$el);

// 执行 mounted 钩子
callHook(vm, 'mounted');
}

// 执行声明周期钩子函数
function callHook(vm, hook) {
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
handlers[i].call(vm);
}
}
}

// Vue 实例初始化渲染函数
// 生成 vnode
Vue.prototype._render = function () {
const vnode = this.$options.render.call(this);
return vnode;
};

// Vue 实例更新函数
Vue.prototype._update = function (vnode) {
// 真实 DOM 更新
const prevVnode = this._vnode;
if (!prevVnode) {
this.$el = patch(null, vnode);
} else {
this.$el = patch(prevVnode, vnode);
}
this._vnode = vnode;
};
  1. 更新阶段:
  • 监听数据变化,当数据发生变化时,触发响应式更新。
    • 数据响应式更新:Vue 通过使用 Object.definePropertyProxy 对数据对象进行劫持,实现了数据的响应式更新。
  • 执行 beforeUpdate 生命周期钩子。
    • 当数据发生变化时,会触发相应的更新钩子。
  • 重新生成 VNode 节点树。
    • 重新渲染虚拟 DOM:数据更新后,Vue 会重新渲染虚拟 DOM 树,生成新的虚拟 DOM。
  • 使用虚拟 DOM 比对新旧 VNode 节点树,找出差异并更新视图。
    • Diff 算法优化:Vue 使用 Diff 算法对比新旧虚拟 DOM,找出需要更新的节点,从而最小化 DOM 操作的次数,提高更新效率。
    • 更新队列管理:Vue 使用更新队列管理数据更新和虚拟 DOM 更新的顺序和优先级,确保更新操作的正确执行顺序。
    • 异步更新策略:Vue 使用异步更新策略来提高性能,将多个数据更新合并成一个更新批次,在下一个事件循环周期中统一执行,避免频繁更新导致性能问题。
    • 性能优化策略:Vue 内部实现了许多性能优化策略,例如通过对静态节点的标记和缓存,减少不必要的更新操作;通过函数式组件等方式降低组件实例化和渲染的开销。
  • 执行 updated 生命周期钩子。
// Vue 实例数据更新方法
function update(vm) {
// 触发数据更新
vm._update(vm._render());

// 执行 beforeUpdate 钩子
callHook(vm, 'beforeUpdate');

// 执行 updated 钩子
callHook(vm, 'updated');
}

// Vue 实例更新函数
Vue.prototype._update = function (vnode) {
// 真实 DOM 更新
const prevVnode = this._vnode;
if (!prevVnode) {
this.$el = patch(null, vnode);
} else {
this.$el = patch(prevVnode, vnode);
}
this._vnode = vnode;
};

// 执行声明周期钩子函数
function callHook(vm, hook) {
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
handlers[i].call(vm);
}
}
}

patch 函数:

function patch(oldVnode, vnode) {
// 如果没有旧的虚拟 DOM,则创建一个新的 DOM 元素
if (!oldVnode) {
return createElm(vnode);
}

// 如果新旧虚拟 DOM 是相同节点类型,则进行 patch 更新
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode);
} else {
// 如果新旧虚拟 DOM 不同类型,则销毁旧的 DOM 元素,创建新的 DOM 元素
const oldElm = oldVnode.elm;
const parent = oldElm.parentNode;
const newElm = createElm(vnode);
parent.insertBefore(newElm, oldElm.nextSibling);
parent.removeChild(oldElm);
}

return vnode.elm;
}

function sameVnode(vnode1, vnode2) {
// 判断两个虚拟 DOM 是否是相同节点类型的条件,例如节点标签名、key、命名空间等
}

function patchVnode(oldVnode, vnode) {
// 对比新旧虚拟 DOM 的子节点,更新子节点的内容
}

function createElm(vnode) {
// 创建真实的 DOM 元素,并递归创建子节点
}
  1. 销毁阶段:
  • 触发 beforeDestroy 钩子
    • 在销毁阶段,Vue 会调用实例的 __call_hook 方法来触发生命周期钩子函数。具体地,在 callHook 函数中会遍历钩子函数数组,并依次执行。
  • 解绑事件、取消订阅等操作
    • 在销毁阶段,Vue 会解绑当前实例上绑定的事件、取消订阅观察者等,防止内存泄漏和无效操作。这个过程是在 destroy 函数中完成的,会遍历所有事件监听器和观察者,并逐个解绑和取消订阅。
  • 递归销毁子组件
    • Vue 在销毁阶段会递归地调用子组件的销毁方法。这个过程是在 destroy 函数中完成的,会依次调用每个子组件实例的 destroy 方法。
  • 销毁实例对象
    • 在销毁阶段,Vue 最终会调用实例的 _cleanup 方法来进行一些清理操作,例如解绑事件、取消订阅、释放内存等。这个过程是在 destroy 函数中完成的。
  • 触发 destroyed 钩子
    • Vue 会在销毁完成后触发 destroyed 钩子函数,表示实例已经彻底销毁。这个过程也是在 callHook 函数中完成的。
function callHook(vm, hook) {
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
handlers[i].call(vm);
}
}
}

function destroyVM(vm) {
callHook(vm, 'beforeDestroy');
// 销毁子组件
vm._children.forEach(destroyVM);
// 解绑事件、取消订阅等操作
cleanup(vm);
// 销毁实例对象
vm.$destroy();
// 触发 destroyed 钩子
callHook(vm, 'destroyed');
}

// 销毁阶段的入口
Vue.prototype.$destroy = function () {
// 一些清理操作
this._cleanup();
};
Vue.prototype._cleanup = function () {
// 解绑事件
this._events = Object.create(null);
// 取消订阅
this._watcher = null;
// 其他清理操作
};

总的来说,Vue.js 在执行过程中主要包括初始化、编译、挂载、更新和销毁等阶段,每个阶段都有相应的生命周期钩子函数可以进行扩展和处理。这些阶段和操作保证了 Vue.js 应用能够正确地初始化、渲染、更新和销毁,实现了数据与视图的同步更新和管理。