Vue的变化侦测(2)

242 阅读4分钟

变化侦测

  • 变化侦测 = 数据观测+依赖收集+依赖更新

    1. 使用Object.defineProperty来使得数据变得可“观测”
    2. 依赖收集(Observer):是指收集视图里的部分与数据绑定的关系
    3. 在getter中收集依赖,在setter中通知更新依赖
    4. 典型的发布-订阅模式,为了解耦,新增了一个管理对象
    5. dep(收集某个数据相关的所有依赖),watcher(被dep通知,更新依赖)
    // observer.js
    // 收集依赖
    const Dep = require('./dep');
    
    export class Observer {
    	constructor(value) {
    		this.value = value;
    		def(value, "__ob__", this);
    		if (Array.isArray(value)) {
    			console.log("array");
    		} else {
    			this.walk(value);
    		}
    	}
    
    	walk(obj) {
    		const keys = Object.keys(obj);
    		for (let i = 0; i < keys.length; i++) {
    			defineReactive(obj,keys[i]);
    		}
    	}
    }
    
    
    function defineReactive(obj, key, val) {
    
        if (arguments.length === 2) {
            val = obj[key];
        }
    
        if (typeof val === 'object') {
            new Observer(val);
        }
    
        const dep = new Dep();
    
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                dep.depend();
                return val;
            },
            set(newval) {
                if (val === newval) return;
                val = newval;
                dep.notify();
            }
        })
    }
    
    
    // dep.js
    
    // 依赖管理器: 1数据 :n依赖 的一对多关系进行依赖管理,收集某个数据相关的所有依赖
    
    export default class Dep {
    
        constructor() {
            this.subs = [];
        }
    
        addSub(sub) {
            this.subs.push(sub);
        }
    
        removeSub(sub) {
            remove(this.subs, sub);
        }
    
        depend() {
            window.target && this.addSub(window.target);
        }
    
        notify() {
            const subs = this.subs.slice();
            for (let i = 0; i < subs.length; i++) {
                subs[i].update();
            }
        }
    
    }
    
    
    export function remove(arr, item) {
    
        if (arr.length > 1) {
            const itemIndex = arr.indexOf(item);
            if (itemIndex > 1) {
                return arr.splice(itemIndex, 1);
            }
        }
        
    }
    
    // watcher.js
    
    //  watcher表示依赖关系,通知视图更新
    
    // window.target是为了拷贝一份 watcher,添加到Dep的依赖数组中
    
    export default class Watcher {
        constructor(vm, expOrFn, cb) {
            this.vm = vm;
            this.cb = cb;
            this.getter = parsePath(expOrFn);
            this.value = this.get();
        }
    
        get() {
            window.target = this;
            const vm = this.vm;
            let value = this.getter.call(vm, vm);
            window.target = undefined;
            return value;
        }
    
        update() {
            const oldValue = this.value;
            this.value = this.get();
            this.cb.call(this.vm, this.value, oldValue);
        }
    }
    
    
    /**
     * 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来
     * 例如:
     * data = {a:{b:{c:2}}}
     * parsePath('a.b.c')(data)  // 2
     */
    
    const bailRE = /[^\w.$]/;
    
    export function parsePath(path) {
    
        if (bailRE.test(path)) return;
    
        const segements = path.split('.');
    
        return function (obj) {
            for (let i = 0; i < segements.length; i++) {
                if (!obj) return;
                obj = obj[segements[i]];
            }
            return obj;
        }
    
    }
    

    侦测流程

    vue这套变化侦测的缺点很明显,因为利用defineProperty来进行收集,只限于读和写已有值,当我们对obj进行新增或者删除属性值时,它是监听不到的。所以在官网文档上的叙述上说明过,对数组或对象的直接增加或者删除会产生不期望的结果, 为了解决这一问题,特地增加了Vue.setVue.delete两个全局API 。

    数组怎么办?

    看到这里,对原型熟悉的人可能会问了,这种方法只针对于Obj类型,那剩下的常用的Arr类型或者其他类型呢?defineProperty数组是不可能使用的,那么我们应该怎么对数组进行依赖收集和通知更新?

    还是延续上面的思想:拦截,vue将所有数组的异变方法(能改变原有数组)拦截一波,就能知道arr啥时候被setter了。

    经常面试被问到原型,原型链的what,why,那么how???? 我觉得这就是个很巧妙的实践~

    拦截数组原型上的异变方法(会改变原有宿主的方法)的代码:

    //代码位置 vue/src/core/observer/array.js
    
    /*
     * not type checking this file because flow doesn't play well with
     * dynamically accessing methods on Array prototype
     */
    
    import { def } from '../util/index'
    
    const arrayProto = Array.prototype
    export const arrayMethods = Object.create(arrayProto)
    
    const methodsToPatch = [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ]
    
    /**
     * Intercept mutating methods and emit events
     */
    methodsToPatch.forEach(function (method) {
      // cache original method
      const original = arrayProto[method]
      def(arrayMethods, method, function mutator (...args) {
        const result = original.apply(this, args)
        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)
          
        // notify change
        ob.dep.notify()
        return result
      })
    })
    

数组依赖收集

无论怎样,先得用walk让元素注入observer依赖,使得在getter中实例化Dep收集依赖并将数组方法拦截掉

// 源码位置:/src/core/observer/index.js
const Dep = require("./dep");

const { arrayKeys, arrayMethods } = require("./array");

// 源码位置:src/core/observer/index.js

// 使用 defineProperty 让数据可观测

export class Observer {

	constructor(value) {
		this.value = value;
		this.dep = new Dep();
		def(value, "__ob__", this);
		if (Array.isArray(value)) {
			const agument = hasProto ? protoAugment : copyAugument;
			[agument](value, arrayMethods, arrayKeys);
			this.observerArray(value);
		} else {
			this.walk(value);
		}
	}

	walk(obj) {
		const keys = Object.keys(obj);
		for (let i = 0; i < keys.length; i++) {
			defineReactive(obj, keys[i]);
		}
	}

	observerArray(ietms) {
		for (let i = 0; i < ietms.length; i++) {
			observe(ietms[i]);
		}
	}
}

export const hasProto = "__proto__" in {};

/* 
    复制原型属性,添加拦截
*/

function protoAugment(target, src, keys) {
	target.__proto__ = src;
}

function copyAugument(target, src, keys) {
	for (let i = 0; i < keys.length; i++) {
		const key = key[i];
		def(target, key, src[key]);
	}
}

/*
 *  尝试为value创建一个0bserver实例,如果创建成功,直接返回新创建的Observer实例。
 *  如果 Value 已经存在一个Observer实例,则直接返回它
 */

function observe(value) {
	if (!isObject(value) || value instanceof VNode) {
		return;
	}
	let ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__;
    } else {
        ob = new Observer(value);
    }
    return ob;
}

function defineReactive(obj, key, val) {
	let childOb = observe(val);

	if (arguments.length === 2) {
		val = obj[key];
	}

	if (typeof val === "object") {
		new Observer(val);
	}

	const dep = new Dep();

	Object.defineProperty(obj, key, {
		enumerable: true,
		configurable: true,
		get() {
			if (childOb) {
				childOb.dep.depend();
			}
			return val;
		},
		set(newval) {
			if (val === newval) return;
			val = newval;
			dep.notify();
		}
	});
}

通知更新

主要是还要对数组进行深度监测和新增元素侦测,在拦截的原型上进行依赖更新。

__ob__是在进行初始化observer的时候,在被监听者上面挂载了自己的实例,以便访问后进行依赖更新。

// 源码位置:vue/src/core/observer/array.js


/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    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)
    // notify change
    ob.dep.notify()
    return result
  })
})
  • 总结: vue的变化侦测与React对比Vdom和Angular的脏值检测都不一样。核心是利用defineProperty的能力,拦截所有绑定的响应式数据(data中),在拦截中添加依赖管理器Dep来收集管理依赖,用Watcher表示依赖关系本身,进行通知依赖更新。

    其中,对于数组的侦测的思路是,覆盖所有数组原型的的异变方法,在覆盖后植入依赖逻辑。这套缺点就是对数组进行下标赋值操作时,vue是侦测不到的,官网文档上多处对此有说明。