响应式原理
响应式原理主要包括两大主要部分,包括找出状态更新后需要更新的组件,以及采用dom-diff最小化更新这些组件的dom
1. 寻找需要更新的组件
目前三大框架中对数据变化的侦测主要有两种,react采用setState的方式命令式的通知框架,框架内部收集到信号后,使用dom-diff暴力比对,自触发setState的组件开始,递归向下比对,找出需要更新的组件,修改他们的dom。
vue采用代理模式与观察者模式相结合的方式,能够精确通知到变化数据对应的组件,仅在这些组件内进行dom-diff 比对,更新dom。
代理模式的具体实现为使用Object.defineproperty或者proxy 为开发者提供了向对象属性的获取和修改操作嵌入额外行为的能力。
Object.defineProperty(data, key, {
get() {
if(Dep.target){ // 如果取值时有watcher
dep.depend(); // 让watcher保存dep,并且让dep 保存watcher
}
return value
},
set(newValue) {
if (newValue == value) return;
observe(newValue);
value = newValue;
dep.notify(); // 通知渲染watcher去更新
}
});
观察者模式的具体实现为,在被观察者中存放使用该数据的组件的跟新逻辑,当这个数据跟新时,循环调用这些方法。
class Observerable {
constructor() {
this.observers = []
}
add(observer) {
this.observers.push(observer)
}
notify() {
for(let i=0;i<this.observers.length;i++) {
this.observers[i].update()
}
}
}
class Observer {
constructor() {
this.subs = []
this.subIds = new Set()
}
addSubject(sub) { //添加被订阅者
this.subs.push(sub)
sub.add(this)
}
update() {
console.log('状态更新了')
}
}
整体实现为在组件初渲染时声明watcher实例,把这个watcher实例绑定到Dep类的静态属性上,调用render方法时读取数据,触发getter,把Dep.target上绑定的watcher收集起来,当数据更新时触发setter,调用notify方法通知到watcher.update方法,经过异步更新流程后,调用patch方法使用dom-diff 进行比对。
let id = 0;
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm;
this.exprOrFn = exprOrFn;
if (typeof exprOrFn == 'function') {
this.getter = exprOrFn;
}
this.cb = cb;
this.options = options;
this.id = id++;
this.get();
}
get() {
pushTarget(this)
this.getter.call(vm)
popTarget()
}
update() {
this.get()
}
}
export default Watcher;
2. 通过dom-diff更新dom节点
之所以采用dom-diff,是因为依赖收集无法精确到dom元素级别,react中没有依赖收集,vue中收集到dom元素级别,内存和性能实现成本过高,因此采用更粗的颗粒度,监控到组件级别,这就需要dom-diff来暴力比对,获取到需要更新的dom元素。
function patch (oldVnode, vnode, parentElm) {
if(oldVnode == null) {
addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
return
}
if(vnode == null) {
removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
return
}
if(!sameVnode(oldVNode, vnode)) {
removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
return
}
patchVnode(oldVNode, vnode);
}
类比两颗树的比较
- 首先判断新老根节点是否都不存在,如果都不存在直接返回即可
- 如果老节点不存在,而新节点存在,把新节点添加到父元素
- 如果老节点存在,而新节点不存在,,删除父元素的子节点
- 如果新老节点都存在,则比较是否相同,如果不相同,则删除老的,添加新的
- 如果都存在且相同,则开始比较子元素
- 如果新节点是一个文本元素,则无子元素,直接使用当前文本进行替换
- 如果老元素有儿子节点而新元素无儿子节点,删除老元素的儿子节点
- 如果新元素有儿子节点,而老元素无儿子节点,把儿子元素添加到老节点中
- 如果都有节点,进入到patchVnode阶段
9.1 - 优化策略
- 设置newStartIndex, newEndIndex, oldStartIndex, oldEndIndex 及其相对应的节点
- 只要索引没有越界一直循环
- 新前与旧前比较,如果命中则更新旧节点属性
- 新后与旧后比较,如果命中则更新旧节点属性
- 新后与旧前比较,如果命中则把当前元素移动到老节点所有未处理的最后面。
- 新前与旧后比较,如果命中则把当前元素移动到老节点所有未处理节点的最后面
- 如果都没有命中进行循环比较,
- 依次取出每一个新子节点,去老元素中通过可以判断是否存在,如果存在就移动到旧前的最前面,不存在就创建后再移动到旧前的最前面。
9.2 暴力比对
比较完成后,如果newStartIndex < newEndIndex,说明新节点还有剩下的的,则遍历newStartIndex—newEndIndex插入到老节点的儿子节点尾部。
比较完成后,如果oldStartIndex < oldEndIndex, 说明老节点还有剩下的,则遍历oldStartIndex < oldEndIndex,然后清除掉。