跳到主要内容

认识

2024年03月13日
柏拉文
越努力,越幸运

一、认识


二、初始化


  1. 定义 Vue.prototype._initVue.prototype.$dataVue.prototype.$propsVue.prototype.$setVue.prototype.$deleteVue.prototype.$watchVue.prototype.$onVue.prototype.$onceVue.prototype.$offVue.prototype.$emitVue.prototype._updateVue.prototype.$forceUpdateVue.prototype.$destroyVue.prototype.$nextTickVue.prototype._render等方法

  2. 执行 Vue.prototype._init 函数

  3. initInternalComponent() 或者 mergeOptions 处理组件配置项

    • initInternalComponent(): 每个子组件初始化时走这里,这里只做了一些性能优化, 将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率

    • mergeOptions: 初始化根组件时走这里,合并 Vue 的全局配置到根组件的局部配置,比如 Vue.component 注册的全局组件会合并到 根实例的 components 选项中

  4. initLifecycle(vm) 初始化组件实例的关系属性,比如 $parent$children$root$refs

  5. initEvents(vm) 处理自定义事件: 这里需要注意一点,所以我们在 <comp @click="handleClick" /> 上注册的事件,监听者不是父组件,而是子组件本身,也就是说事件的派发和监听者都是子组件本身,和父组件无关

  6. initRender(vm) 解析组件的插槽信息,得到 vm.$slot,处理渲染函数,得到 vm.$createElement 方法,即 h 函数

  7. callHook(vm, 'beforeCreate', undefined, false /* setContext */) 调用 beforeCreate 钩子函数: 数据初始化并未完成,像dataprops这些属性无法访问到

  8. initInjections(vm) 初始化组件的 inject 配置项,得到 result[key] = val 形式的配置对象,然后对结果数据进行响应式处理,并代理每个 keyvm 实例

  9. initState(vm) 处理数据响应式,处理 propsmethodsdatacomputedwatch

  10. initProvide(vm) 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上

  11. callHook(vm, 'created') 调用 created 钩子函数: 数据已经初始化完成,能够访问dataprops这些属性,但这时候并未完成dom的挂载,因此无法访问到dom元素

  12. 如果发现配置项上有 el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,就不需要再手动调用 $mount 方法,反之,没提供 el 选项则必须调用 $mount

  13. 接下来则进入挂载阶段

三、挂载


  1. 调用 $mount , $mount 主要有以下工作:

    1. 处理 render 选项或者 template 选项, 如果两个都没有, 通过 el 获取 DOM 上的 outerHTML 字符串

    2. 通过 compileToFunctions 编译 template , 生成 render 函数 和 staticRenderFns 并挂载到 Vue.$options

  2. 调用 $mount > Vue.prototype.$mount > mountComponent

  3. mountComponent 函数中,主要有以下工作:

    1. callHook(vm, 'beforeMount') 执行 beforeMount 钩子

    2. 定义 updateComponent 函数, updateComponent 函数可以渲染 VNode

    3. 实例化渲染 Watcher, 将updateComponent 作为第二个参数传入, updateComponent 后续会作为 Watchergetter 函数。

  4. Watcherconstructor 构造函数中, 初始调用 this.getter 函数, 进而调用执行 updateComponent 函数进行首次渲染

  5. updateComponent 函数中, 主要有以下工作:

    1. 执行 vm._render 生成组件的 VNode: 在组件渲染的过程中, 用到的数据属性会触发 getter, getter 内部会收集依赖。

    2. 执行 vm.__patch__ 进行首次渲染

  6. 递归遍历 VNode , 创建各个节点,处理节点上的属性和指令, 如果是自定义组件则创建组件实例, 进行组件的初始化、挂载

  7. 最终所有 VNode 变成真实的 DOM 节点并替换掉页面上的模版内容

  8. 完成初始渲染

四、更新


  1. 响应式拦截到数据的更新, setter 拦截到更新操作

  2. 调用 dep.notify() , 循环遍历 subs 中的所有 Watcher, 执行 Watcherupdate 方法。

  3. update 方法中, 会将此时的 Watcher 加入到渲染队列 queue, 通过 nextTick 进行批量更新渲染

  4. nextTick 中,通过 promise.then(flushCallbacks) 将批量更新任务放到了微任务队列, 依次执行任务队列中的任务, 每一个任务就是一个 Watcher, 开始执行每一个 Watcherrun 方法

  5. Watcher 中的 run 方法调用 Watcher 中的 get 方法, get 方法调用 Watcher 中的 getter 函数, 此时的 getter 函数就是 updateComponent , 用于初始或者更新渲染

  6. 首先执行 vm._render 生成组件的 vnode,这时就会执行编译器生成的函数

  7. 执行 vm.__patch__ 进行更新渲染

  8. 执行 patchVnode 进行 VNode Diff 操作

  9. 完成更新

五、思考与沉淀


5.1 什么是 MVVM?

MVVM(Model-View-ViewModel)是一种软件架构模式,用于实现用户界面(UI)和业务逻辑的分离。它的设计目标是将界面的开发与后端的业务逻辑分离,使代码更易于理解、维护和测试。

在MVVM中,各个组成部分的职责如下:

Model(模型):表示应用程序的数据和业务逻辑。它负责数据的存储、检索和更新,并封装了与数据相关的操作和规则。 View(视图):展示用户界面,通常是由UI元素组成的。它是用户与应用程序进行交互的界面,负责将数据呈现给用户,并接收用户的输入。 ViewModel(视图模型):连接View和Model,负责处理业务逻辑和数据的交互。它从Model中获取数据,并将数据转换为View可以理解和展示的格式。ViewModel还负责监听View的变化,并根据用户的输入更新Model中的数据。

MVVM的核心思想是数据绑定,通过双向绑定机制将View和ViewModel中的数据保持同步。当ViewModel中的数据发生变化时,View会自动更新,反之亦然。这种数据驱动的方式使得开发者可以专注于业务逻辑的实现,而无需手动操作DOM元素来更新界面。

MVVM的优势包括:

可维护性:将界面逻辑与业务逻辑分离,使代码更易于理解和维护。 可测试性:由于视图逻辑与业务逻辑解耦,可以更容易地编写单元测试来验证ViewModel的行为。 可复用性:ViewModel可以独立于具体的View,可以复用在不同的界面上,提高代码的重用性。 团队协作:MVVM模式将界面开发与后端逻辑分离,使得前端和后端开发人员可以并行工作,提高团队的协作效率。 总而言之,MVVM是一种能够将界面逻辑与业务逻辑分离的软件架构模式,通过数据绑定实现了View和ViewModel的自动同步,提高了代码的可维护性、可测试性和可复用性。

在Vue中,ViewModel由Vue实例扮演。Vue通过数据绑定机制建立了View和ViewModel之间的连接,当ViewModel中的数据发生变化时,View会自动更新,反之亦然。这种双向数据绑定使得开发者能够以一种声明式的方式编写代码,而不需要手动操作DOM来更新界面。

总结来说,MVVM是一种将数据驱动视图的设计模式,通过ViewModel作为中间层来实现数据和视图之间的解耦。Vue作为一种流行的MVVM框架,提供了强大的数据绑定和响应式系统,使开发者能够更轻松地构建交互性强的Web应用程序。

数据劫持 Vue 内部使用了 Obeject.defineProperty() 来实现双向绑定,通过这个函数可以监听到 set 和 get的事件

var data = { name: 'poetry' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value

function observe(obj) {
// 判断类型
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key])
})
}

function defineReactive(obj, key, val) {
// 递归子属性
observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get value')
return val
},
set: function reactiveSetter(newVal) {
console.log('change value')
val = newVal
}
})
}

以上代码简单的实现了如何监听数据的 set 和 get 的事件,但是仅仅如此是不够的,还需要在适当的时候给属性添加发布订阅

<div>
{{name}}
</div>

在解析如上模板代码时,遇到 {name} 就会给属性 name 添加发布订阅

// 通过 Dep 解耦
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
// sub 是 Watcher 实例
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null

function update(value) {
document.querySelector('div').innerText = value
}

class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 调用 update 方法更新 Dom
this.cb(this.value)
}
}
var data = { name: 'poetry' }
observe(data)
// 模拟解析到 `{{name}}` 触发的操作
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy'

接下来,对 defineReactive 函数进行改造

function defineReactive(obj, key, val) {
// 递归子属性
observe(val)
let dp = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get value')
// 将 Watcher 添加到订阅
if (Dep.target) {
dp.addSub(Dep.target)
}
return val
},
set: function reactiveSetter(newVal) {
console.log('change value')
val = newVal
// 执行 watcher 的 update 方法
dp.notify()
}
})
}

以上实现了一个简易的双向绑定,核心思路就是手动触发一次属性的 getter 来实现发布订阅的添加

Proxy 与 Obeject.defineProperty 对比

Object.defineProperty在实现双向绑定时存在一些局限性,特别是在处理数组时的表现。为了解决这些问题,JavaScript引入了Proxy对象,它提供了更强大的拦截和自定义行为能力,进一步改善了双向绑定的实现。

与Object.defineProperty相比,Proxy具有以下优势:

支持监听数组变化:使用Proxy可以监听到数组的变化,包括对数组的push、pop、splice等操作。这使得在实现数组的双向绑定时更加方便和高效。 支持监听动态新增属性:Proxy可以监听对象属性的动态新增,而Object.defineProperty只能监听已经存在的属性。这意味着可以在运行时动态地给对象添加新属性,并对其进行拦截和处理。 更灵活的拦截和自定义行为:Proxy提供了多种拦截器(handler),可以针对不同的操作进行自定义处理。通过拦截器,可以实现属性的读取、设置、删除等操作的拦截,以及对函数的调用进行拦截。这种灵活性使得在实现双向绑定时更加便捷和可控。 然而,需要注意的是,Proxy是ES6引入的新特性,对于一些较旧的浏览器可能不完全支持。在选择使用Proxy还是Object.defineProperty时,需要根据目标平台和需求进行权衡和选择。

总结来说,Proxy相比Object.defineProperty提供了更强大和灵活的拦截和自定义行为能力,特别是在处理数组和动态新增属性时表现更好。它是实现双向绑定的一种更先进的方法,为开发者提供了更好的开发体验和效率。

以下是一个简单的示例代码,演示了如何使用Proxy实现简单的双向绑定功能。

// 定义一个响应式对象
const reactiveObj = {
name: 'poetry',
age: 30
};

// 创建一个代理对象
const reactiveProxy = new Proxy(reactiveObj, {
get(target, key) {
console.log(`读取属性 ${key}`);
return target[key];
},
set(target, key, value) {
console.log(`设置属性 ${key} 值为 ${value}`);
target[key] = value;
// 触发更新操作,这里简化为输出当前对象
console.log(reactiveObj);
return true;
}
});

// 使用代理对象进行属性的读取和设置
console.log(reactiveProxy.name); // 读取属性 name
reactiveProxy.age = 40; // 设置属性 age 值为 40

在上述示例中,我们使用Proxy创建了一个代理对象reactiveProxy,并定义了get和set拦截器。在get拦截器中,我们输出了属性的读取操作,而在set拦截器中,我们输出了属性的设置操作,并手动触发了更新操作。通过代理对象reactiveProxy,我们可以像访问普通对象一样读取和设置属性值,同时还可以进行自定义的操作。

在Vue.js中,实际的双向绑定实现比上述示例要复杂得多,涉及到依赖追踪、响应式系统、模板编译等方面的内容。Vue.js使用了Proxy对象和其他技术来实现双向绑定功能。如果你有兴趣深入了解Vue.js的源码实现,可以查看Vue.js的官方仓库,其中包含了完整的源码实现。

5.1 Vue 的编译过程的设计思想?

Vue.js 在不同的平台下都会有编译的过程, 因此编译过程中的依赖的配置 baseOptions 会有所不同。而编译过程会多次执行,但这同一个平台下每一次的编译过程配置又是相同的,为了不让这些配置在每次编译过程都通过参数传入, Vue.js 利用了函数柯里化的技巧很好的实现了 baseOptions 的参数保留。同样,Vue.js 也是利用函数柯里化技巧把基础的编译过程函数抽出来,通过 createCompilerCreator(baseCompile) 的方式把真正编译的过程和其它逻辑如对编译配置处理、缓存处理等剥离开

5.2 Vue 在订阅依赖时所做的优化?

考虑到一种场景,我们的模板会根据 v-if 去渲染不同子模板 ab,当我们满足某种条件的时候渲染 a 的时候,会访问到 a 中的数据,这时候我们对 a 使用的数据添加了 getter,做了依赖收集,那么当我们去修改 a 的数据的时候,理应通知到这些订阅者。那么如果我们一旦改变了条件渲染了 b 模板,又会对 b 使用的数据添加了 getter,如果我们没有依赖移除的过程,那么这时候我去修改 a 模板的数据,会通知 a 数据的订阅的回调,这显然是有浪费的。

因此 Vue 设计了在每次添加完新的订阅,会移除掉旧的订阅,这样就保证了在我们刚才的场景中,如果渲染 b 模板的时候去修改 a 模板的数据,a 数据订阅回调已经被移除了,所以不会有任何浪费,真的是非常赞叹 Vue 对一些细节上的处理。

5.3 Vue 是如何优雅的处理不同平台不同的 patch?

5.4 Vue 是如何将 Dep 类 和 Watcher 类建立联系?

5.5 Vue 2.0 响应式原理

整体思路是数据劫持+观察者模式

对象内部通过 defineReactive 方法,使用 Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)。

class Observer {
// 观测值
constructor(value) {
this.walk(value);
}
walk(data) {
// 对象上的所有属性依次进行观测
let keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
let value = data[key];
defineReactive(data, key, value);
}
}
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
observe(value); // 递归关键
// --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
// 思考?如果Vue数据嵌套层级过深 >>性能会受影响
Object.defineProperty(data, key, {
get() {
console.log("获取值");

//需要做依赖收集过程 这里代码没写出来
return value;
},
set(newValue) {
if (newValue === value) return;
console.log("设置值");
//需要做派发更新过程 这里代码没写出来
value = newValue;
},
});
}
export function observe(value) {
// 如果传过来的是对象或者数组 进行属性劫持
if (
Object.prototype.toString.call(value) === "[object Object]" ||
Array.isArray(value)
) {
return new Observer(value);
}
}

属性带理vm.xxx -> options.data.xxx,编译阶段收集依赖 + 观察者模式 + Object.defineProperty的getter中添加观察者,setter发布更新;vue2.x的虚拟dom,通过与上一次缓存的虚拟dom进行 diff 差异对比,最小化更新(一个组件一个观察者,比vue1.x的粒度粗一些,节省了性能)

5.6 Vue 2.0 双向数据绑定

双向数据绑定 基于 MVVM 模型, 包括数据层Model视图层View业务逻辑层ViewModel。其中 ViewModel(业务逻辑层) 负责将数据和视图关联起来, 提供了数据变化后更新视图视图变化后更新数据这样一个功能,就是传统意义上的双向绑定。

Vue.js 2.0: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。Vue的双向绑定数据的原理是基于数据劫持和发布者-订阅者模式的组合。

具体步骤如下:

Vue通过Object.defineProperty()方法对数据对象进行劫持。 在劫持过程中,为每个属性添加了getter和setter。 当访问属性时,会触发getter函数,而当属性值发生变化时,会触发setter函数。 在setter函数中,Vue会通知相关的订阅者,即依赖于该属性的视图或其他数据。 订阅者收到通知后,会执行相应的更新操作,将新的数据反映到视图上。 这样,当数据发生变化时,Vue能够自动更新相关的视图,实现了双向绑定的效果。

这种原理结合了数据劫持和发布者-订阅者模式的特点,实现了数据与视图之间的自动同步。通过数据劫持,Vue能够捕获数据的变化,而发布者-订阅者模式则确保了数据变化时的及时通知和更新。

// 定义一个数据对象
const data = {
message: 'Hello Vue!',
};

// 通过Object.defineProperty()劫持数据对象
Object.defineProperty(data, 'message', {
get() {
console.log('访问数据');
return this._message;
},
set(newValue) {
console.log('更新数据');
this._message = newValue;
// 通知订阅者,执行更新操作
notifySubscribers();
},
});

// 定义一个订阅者列表
const subscribers = [];

// 订阅者订阅数据
function subscribe(callback) {
subscribers.push(callback);
}

// 通知订阅者,执行更新操作
function notifySubscribers() {
subscribers.forEach((callback) => {
callback();
});
}

// 订阅者更新视图
function updateView() {
console.log('视图更新:', data.message);
}

// 订阅数据变化
subscribe(updateView);

// 修改数据,触发更新
data.message = 'Hello VueJS!';

在上述示例中,我们通过Object.defineProperty()对data对象的message属性进行劫持,并在getter和setter中添加了相应的日志和更新操作。订阅者通过subscribe方法订阅数据变化,并在updateView方法中更新视图。当我们修改data.message的值时,会触发setter函数,从而通知订阅者执行更新操作,最终更新了视图。

参考资料


Vue.js 技术揭秘