认识
一、认识
二、初始化
-
定义
Vue.prototype._init
、Vue.prototype.$data
、Vue.prototype.$props
、Vue.prototype.$set
、Vue.prototype.$delete
、Vue.prototype.$watch
、Vue.prototype.$on
、Vue.prototype.$once
、Vue.prototype.$off
、Vue.prototype.$emit
、Vue.prototype._update
、Vue.prototype.$forceUpdate
、Vue.prototype.$destroy
、Vue.prototype.$nextTick
、Vue.prototype._render
等方法 -
执行
Vue.prototype._init
函数 -
initInternalComponent()
或者mergeOptions
处理组件配置项-
initInternalComponent()
: 每个子组件初始化时走这里,这里只做了一些性能优化, 将组件配置对象上的一些深层次属性放到vm.$options
选项中,以提高代码的执行效率 -
mergeOptions
: 初始化根组件时走这里,合并Vue
的全局配置到根组件的局部配置,比如Vue.component
注册的全局组件会合并到 根实例的components
选项中
-
-
initLifecycle(vm)
初始化组件实例的关系属性,比如$parent
、$children
、$root
、$refs
等 -
initEvents(vm)
处理自定义事件: 这里需要注意一点,所以我们在<comp @click="handleClick" />
上注册的事件,监听者不是父组件,而是子组件本身,也就是说事件的派发和监听者都是子组件本身,和父组件无关 -
initRender(vm)
解析组件的插槽信息,得到vm.$slot
,处理渲染函数,得到vm.$createElement
方法,即h
函数 -
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
调用beforeCreate
钩子函数: 数据初始化并未完成,像data
、props
这些属性无法访问到 -
initInjections(vm)
初始化组件的inject
配置项,得到result[key] = val
形式的配置对象,然后对结果数据进行响应式处理,并代理每个key
到vm
实例 -
initState(vm)
处理数据响应式,处理props
、methods
、data
、computed
、watch
-
initProvide(vm)
解析组件配置项上的provide
对象,将其挂载到vm._provided
属性上 -
callHook(vm, 'created')
调用created
钩子函数: 数据已经初始化完成,能够访问data
、props
这些属性,但这时候并未完成dom
的挂载,因此无法访问到dom
元素 -
如果发现配置项上有
el
选项,则自动调用$mount
方法,也就是说有了el
选项,就不需要再手动调用$mount
方法,反之,没提供el
选项则必须调用$mount
-
接下来则进入挂载阶段
三、挂载
-
调用
$mount
,$mount
主要有以下工作:-
处理
render
选项或者template
选项, 如果两个都没有, 通过el
获取DOM
上的outerHTML
字符串 -
通过
compileToFunctions
编译template
, 生成render
函数 和staticRenderFns
并挂载到Vue.$options
上
-
-
调用
$mount > Vue.prototype.$mount > mountComponent
-
在
mountComponent
函数中,主要有以下工作:-
callHook(vm, 'beforeMount')
执行beforeMount
钩子 -
定义
updateComponent
函数,updateComponent
函数可以渲染VNode
-
实例化渲染
Watcher
, 将updateComponent
作为第二个参数传入,updateComponent
后续会作为Watcher
的getter
函数。
-
-
在
Watcher
的constructor
构造函数中, 初始调用this.getter
函数, 进而调用执行updateComponent
函数进行首次渲染 -
在
updateComponent
函数中, 主要有以下工作:-
执行
vm._render
生成组件的VNode
: 在组件渲染的过程中, 用到的数据属性会触发getter
,getter
内部会收集依赖。 -
执行
vm.__patch__
进行首次渲染
-
-
递归遍历
VNode
, 创建各个节点,处理节点上的属性和指令, 如果是自定义组件则创建组件实例, 进行组件的初始化、挂载 -
最终所有
VNode
变成真实的DOM
节点并替换掉页面上的模版内容 -
完成初始渲染
四、更新
-
响应式拦截到数据的更新,
setter
拦截到更新操作 -
调用
dep.notify()
, 循环遍历subs
中的所有Watcher
, 执行Watcher
的update
方法。 -
在
update
方法中, 会将此时的Watcher
加入到渲染队列queue
, 通过nextTick
进行批量更新渲染 -
在
nextTick
中,通过promise.then(flushCallbacks)
将批量更新任务放到了微任务队列, 依次执行任务队列中的任务, 每一个任务就是一个Watcher
, 开始执行每一个Watcher
的run
方法 -
Watcher
中的run
方法调用Watcher
中的get
方法,get
方法调用Watcher
中的getter
函数, 此时的getter
函数就是updateComponent
, 用于初始或者更新渲染 -
首先执行
vm._render
生成组件的vnode
,这时就会执行编译器生成的函数 -
执行
vm.__patch__
进行更新渲染 -
执行
patchVnode
进行VNode
Diff
操作 -
完成更新
五、思考与沉淀
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
去渲染不同子模板 a
和 b
,当我们满足某种条件的时候渲染 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函数,从而通知订阅者执行更新操作,最终更新了视图。