vue 响应式原理

333 阅读4分钟

响应式原理

响应式原理主要包括两大主要部分,包括找出状态更新后需要更新的组件,以及采用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);
}

类比两颗树的比较

  1. 首先判断新老根节点是否都不存在,如果都不存在直接返回即可
  2. 如果老节点不存在,而新节点存在,把新节点添加到父元素
  3. 如果老节点存在,而新节点不存在,,删除父元素的子节点
  4. 如果新老节点都存在,则比较是否相同,如果不相同,则删除老的,添加新的
  5. 如果都存在且相同,则开始比较子元素
  6. 如果新节点是一个文本元素,则无子元素,直接使用当前文本进行替换
  7. 如果老元素有儿子节点而新元素无儿子节点,删除老元素的儿子节点
  8. 如果新元素有儿子节点,而老元素无儿子节点,把儿子元素添加到老节点中
  9. 如果都有节点,进入到patchVnode阶段

9.1 - 优化策略

    - 设置newStartIndex, newEndIndex, oldStartIndex, oldEndIndex 及其相对应的节点
    - 只要索引没有越界一直循环
    - 新前与旧前比较,如果命中则更新旧节点属性
    - 新后与旧后比较,如果命中则更新旧节点属性
    - 新后与旧前比较,如果命中则把当前元素移动到老节点所有未处理的最后面。
    - 新前与旧后比较,如果命中则把当前元素移动到老节点所有未处理节点的最后面
    - 如果都没有命中进行循环比较,
      - 依次取出每一个新子节点,去老元素中通过可以判断是否存在,如果存在就移动到旧前的最前面,不存在就创建后再移动到旧前的最前面。

9.2 暴力比对

比较完成后,如果newStartIndex < newEndIndex,说明新节点还有剩下的的,则遍历newStartIndex—newEndIndex插入到老节点的儿子节点尾部。

比较完成后,如果oldStartIndex < oldEndIndex, 说明老节点还有剩下的,则遍历oldStartIndex < oldEndIndex,然后清除掉。