理解vue的设计思想
MVVM模式
MVVM框架的三要素:数据响应式、模版引擎及其渲染
数据响应式:监听数据变化并在视图中更新
- Object.defineProperty()
- Proxy
模版引擎:提供描述视图的模版语法
- 插值:{{}}
- 指令:v-bind,v-on,v-model,v-for,v-if
渲染:如何将模版转换为html
- 模版 =>vdom =>dom
vue数据双向绑定原理
原理分析
1. new Vue() 首先执行初始化,对data执行响应化处理,这个过程发生在Observer中
2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中
3. 同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
4. 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个Watcher
5. 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数
思路分析:视图到数据的改动要监听DOM的变化再同步赋值给变量,比如给input添加change或input监听事件并在事件处理函数中给变量赋值,实际也就是v-model指令在做的事,另一个方向,监听数据变化,再去更新对应的DOM,这里的监听及处理也就是vue的实现方式——发布订阅模式+数据劫持(Object.defineProperty)。
- 监听数据变化(数据劫持/数据代理)
- 收集视图依赖了哪些数据 (依赖收集)
- 数据变化时,自动“通知”视图需要修改哪些部分,并进行更新 (发布订阅模式)
简单版双向绑定的实现
遍历需要响应化的对象
// 对象响应化:遍历每个key,定义getter、setter
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
解决嵌套对象问题
function defineReactive(obj, key, val) {
observe(val)
Object.defineProperty(obj, key, {
//...
解决赋值是对象的情况
set(newVal) {
if (newVal !== val) {
observe(newVal) // 新值是对象的情况
notifyUpdate()
1、Observer—数据监听系统,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者,核心方法就是Object.defineProperty( )
function defineReactive(data, key, val) {
observe(val); // 递归遍历所有子属性
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
return val;
},
set: function(newVal) {
val = newVal;
console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
}
});
}
function observe(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
});
};
2、Dep —发布订阅模型,作为连接Observer和Compile的桥梁,一个Dep实例对应一个对象属性或一个被观察的对象,能够订阅并收集每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。
function defineReactive(data, key, val) {
observe(val); // 递归遍历所有子属性
var dep = new Dep(); // dep
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
if (是否需要添加订阅者) {
dep.addSub(watcher); // 在这里添加一个订阅者
}
return val;
},
set: function(newVal) {
if (val === newVal) {
return;
}
val = newVal;
console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
dep.notify(); // 如果数据变化,通知所有订阅者
}
});
}
function Dep () {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
3、Watcher—订阅者Watcher在初始化的时候需要将自己添加进订阅器Dep中
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.value = this.get(); // 将自己添加到订阅器的操作
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
Dep.target = this; // 缓存自己
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
};
然后需要对监听器Observer稍微调整,主要是对应watcher类原型上的get方法
function defineReactive(data, key, val) {
observe(val); // 递归遍历所有子属性
var dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
if (Dep.target) {. // 判断是否需要添加订阅者
dep.addSub(Dep.target); // 在这里添加一个订阅者
}
return val;
},
set: function(newVal) {
if (val === newVal) {
return;
}
val = newVal;
console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
dep.notify(); // 如果数据变化,通知所有订阅者
}
});
}
Dep.target = null;
4、Compile—指令解析系统,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
if (this.$el) {
this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
if (this.isElement(node)) {
console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
console.log("编译插值文本" + node.textContent);
}
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
});
}
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
VUE 源码所对应位置
- core/observer/index.js observe() 返回一个Observer实例
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 观察者
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {// 创建观察者
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
- core/observer/index.js Observer对象根据数据类型执行对应的响应化操作
export class Observer {
value: any;
dep: Dep; // 保存数组类型数据的依赖
constructor (value: any) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this) // 在getter中可以通过__ob__可获取ob实例
if (Array.isArray(value)) { // 数组响应化
protoAugment(value, arrayMethods)
this.observeArray(value)
} else { // 对象响应化
this.walk(value)
}
}
/**
* 遍历对象所有属性定义其响应化
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* 对数组每一项执行observe
*/
observeArray (items: Array) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
- core/observer/dep.js Dep负责管理一组Watcher,包括watcher实例的增删及通知更新
export default class Dep {
static target: ?Watcher; // 依赖收集时的wacher引用
subs: Array; // watcher数组
constructor () {
this.subs = []
}
//添加watcher实例
addSub (sub: Watcher) {
this.subs.push(sub)
}
//删除watcher实例
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
//watcher和dep相互保存引用
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
- src/core/observer/watcher.js watcher监控一个表达式或关联一个组件更新函数,数值更新则指定回调或更新函数被调用
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
// 组件保存render watcher
if (isRenderWatcher) {
vm._watcher = this
}
// 组件保存非render watcher
vm._watchers.push(this)
// options...
// 将表达式解析为getter函数
// 如果是函数则直接指定为getter,那什么时候是函数?
// 答案是那些和组件实例对应的Watcher创建时会传递组件更新函数updateComponent
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 这种是$watch传递进来的表达式,它们需要解析为函数
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
}
}
// 若非延迟watcher,立即调用getter
this.value = this.lazy ? undefined : this.get()
}
/**
* 模拟getter, 重新收集依赖re-collect dependencies.
*/
get () {
// Dep.target = this
pushTarget(this)
let value
const vm = this.vm
try {
// 从组件中获取到value同时触发依赖收集
value = this.getter.call(vm, vm)
}
catch (e) {}
finally {
// deep watching,递归触发深层属性
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
// watcher保存dep引用
this.newDepIds.add(id)
this.newDeps.push(dep)
// dep添加watcher
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
update () {
// 更新逻辑
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
//默认lazy和sync都是false,所以会走该逻辑
queueWatcher(this)
}
}
}
数组响应化
- src/core/observer/array.js 为数组原型中的7个可以改变内容的方法定义拦截器
// 数组原型
const arrayProto = Array.prototype
// 修改后的原型
export const arrayMethods = Object.create(arrayProto)
// 七个待修改方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* 拦截这些方法,额外发送变更通知
*/
methodsToPatch.forEach(function (method) {
// 原始数组方法
const original = arrayProto[method]
// 修改这些方法的descriptor
def(arrayMethods, method, function mutator (...args) {
// 原始操作
const result = original.apply(this, args)
// 获取ob实例用于发送通知
const ob = this.__ob__
// 三个能新增元素的方法特殊处理
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 若有新增则做响应处理
if (inserted) ob.observeArray(inserted)
// 通知更新
ob.dep.notify()
return result
})
})
- core/observer/index.js 覆盖数组原型
if (Array.isArray(value)) {
// 替换数组原型
protoAugment(value, arrayMethods) // value.__proto__ = arrayMethods
this.observeArray(value)
}
- core/observer/index.js 数组响应式的特殊处理
observeArray (items: Array) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
- core/observer/index.js 依赖收集时的特殊处理
//getter中
if (Array.isArray(value)) {
dependArray(value)
}
// 数组中每一项也需要收集依赖
function dependArray (value: Array) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
整体感知virtual DOM
虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用的各种状态变化会作用于虚拟DOM,最终映射到DOM上。
virtual DOM分为三个步骤:
1.createElement(): 用 JavaScript对象(虚拟树) 描述 真实DOM对象(真实树)
2.diff(oldNode, newNode) : 对比新旧两个虚拟树的区别,收集差异
3.patch() : 将差异应用到真实DOM树
优点
虚拟DOM轻量、快速:当它们发生变化时通过新旧虚拟DOM比对可以得到最小DOM操作量,从而提升性能
跨平台:将虚拟dom更新转换为不同运行时特殊操作实现跨平台
兼容性:还可以加入兼容性代码增强操作的兼容性
必要性
vue 1.0中有细粒度的数据变化侦测,它是不需要虚拟DOM的,但是细粒度造成了大量开销,这对于大型项目来说是不可接受的。因此,vue 2.0选择了中等粒度的解决方案,每一个组件一个watcher实例,这样状态变化时只能通知到组件,再通过引入虚拟DOM去进行比对和渲染。
patchVnode
比较两个VNode,包括三种类型操作:属性更新、文本更新、子节点更新
具体规则如下:
1. 新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren
2. 如果老节点没有子节点而新节点有子节点,先清空老节点的文本内容,然后为其新增子节点。
3. 当新节点没有子节点而老节点有子节点的时候,则移除该节点的所有子节点。
4. 当新老节点都无子节点的时候,只是文本的替换。