评论

vue中watch源码解析

在vue.js中我们通常使用watch去监听数据的变化, 通过分析源码可以帮助我们更好的理解watch的实现原理, 从而更高效的去利用watch实现我们想要的各种效果。

简析vue中的watch

相信大家在使用vue的过程中, 对watch的使用也十分频繁, 下面我会跟着vue源码思路, 对watch进行简单的分析, 以求对watch事件有进一步的了解
1. watch的定义与作用:
侦听(watch)属性的初始化也是发生在 Vue 的实例初始化阶段的 initState 函数中

if(opts.watch && opts.watch !== nativeWatch){
	initWatch(vm, opts.watch)
}

2. initWatch 的实现

function initWatch(vm:Component,watch:object){
	//watch是一个对象, 里面可能会监听多个对象
    for(const key in watch){
    	const handler = watch[key];
        if(Array.isArray(handler)){
        	//判断是否数组
            for(let i=0; i<handler.length; i++){
            	createWatcher(vm,key,handler[i]);
            }
        }else{
            createWatcher(vm,key,handler);
        }
    }
}

3. createWatcher 的简析

function createWatcher(
    vm: Component,  /*当前vue实例*/ 
    expOrFn: string | Function,  
    handler: any,  /*被监听对象的函数*/
    options?: Object  /*option, 待会再讲*/
){
    if(isPlainObject(handler){  //判断是否对象
    	options = handler;
        handler = handler.handler;
    }
    if(typeof handler === 'string'){
    	handler = vm[handler];
    }
    return vm.$watch(expOrFn,handler,options)
}

4. $watch函数的解析

Vue.prototype.$watch = function(
    expOrFn: string | Function,
    cb: any,
    options?: Object /*关于options, 待会再说*/
):Function{
    const vm: Component = this;
    if(isPlainObject(cb)){
    	return createWatcher(vm,expOrFn,cb,options);
        //如果cb还是对象,执行createWatcher(), 直到拿到它最终的回调函数
    }
    options = options || {};
    options.user = true;
    const watcher = new Watcher(vm,expOrFn,cb,options);
    if(options.immediate){
    	cb.call(vm,watcher.value);
    }
    return function unwatchFn(){
    	watcher.teardown(); //移除watcher
    }
}

$watch 的思路解析 :

  • 侦听属性 watch 最终会调用 $watch 方法,这个方法首先判断 cb 如果是一个对象,则调用 createWatcher 方法,接着执行 const watcher = new Watcher(vm, expOrFn, cb, options) 实例化了一个 watcher。

  • 一旦我们 watch 的数据发送变化,它最终会执行回调函数 cb,并且如果我们设置了 immediate 为 true,则直接会执行回调函数 cb。

  • 最后返回了一个 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher

5. Watcher对象
用来解析表达式,收集依赖对象,并在表达式的值变动时执行回调函数

export default class Watcher{
    vm: Component; //实例
    expression: string;  //表达式
    cb: Function;  //回调函数
    id: number;  //watcher实例Id
    deep: boolean; //是否深层
    user: boolean;  //是否用户定义
    computed: boolean;
    sync: boolean;
    dirty: boolean;
    active: boolean;
    dep: Dep;
    deps: Array<Dep>;  //依赖对象数组
    newDeps: Array<Dep>;  //新依赖对象数组
    depIds: SimpleSet;  //依赖id集合
    newDepIds: SimpleSet;  //新依赖id集合
    before: ?Function;
    getter: Function;
    value: any;   //观察值
    
    constructor(
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
    ){
    	this.vm = vm;
        if(isRenderWatcher){
        	vm._watcher = this;
        }
        vm._watchers.push(this);
        // options
        if(options){
            this.deep = !! options.deep;
            this.user = !!options.user;
        }else{
            this.deep = this.user = false;
        }
        this.cb = cb;
        this.id = ++udi;  //uid for batching
        this.active = true;
        this.dirty = this.computed; //for computed watchers
        this.deps = [];
        this.newDeps = [];
        this.depIds = new Set();
        this.newDepIds = new Set();
        this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '';
        //parse expression for getter
        if(typeof expOrFn === 'function'){
            this.getter = expOrFn;
        }else{
            this.getter = parsePath(expOrFn);
            if(!this.getter){
                this.getter = funtion(){ }
                process.env.NODE_ENV !== 'production' && warn(
                    `Fail watching path: "${expOrFn}"` +
                    'Watcher only accepts simple dot-delimited paths. '+
                    'For full control, use a function instead. ',
                    vm
                )
            }
        }
        if(this.computed){
            this.value = undefined;
            this.dep = new Dep();
        }else{
            // 负责调用get方法获取观测值
            this.value = this.get();
        }
    }
}

举例分析 : Watcher中的deep模式

// Watcher options
if(options){
    this.deep = !!options.deep;
}else{
    this.deep = false;
}
var vm = new Vue({
    data(){
    	a:{
            b: 1
        }
    },
    watch:{
        a:{
            handler(newVal){
                console.log(newVal);
            }
        }
    }
})
vm.a.b = 2;

如果b的值改变了,这个时候是不会 log 任何数据的,因为我们是 watch 了 a 对象,只触发了 a 的 getter,并没有触发 a.b 的 getter,所以并没有订阅它的变化,导致我们对 vm.a.b = 2 赋值的时候,虽然触发了 setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了。

6. get( ) 函数
让我们的思路回到watcher中, 在watcher的最后, 我们看到有这么一步, this.value = this.get() , 很明显地,这是调用get()方法来获取检测值, 所以需要我们再去看看这个到底是如何执行的.

// 评估getter, 并重新收集依赖项
get(){
    //将实例添加到watcher栈中
    pushTarget(this);
    let value;
    const vm = this.vm;
    // 尝试调用vm的getter方法
    try{
        value = this.getter.call(vm,vm);
    }catch(e){
        // 捕捉到错误时,跑出异常
        throw e;
    }finally{
        //如果是深度监听,将他们全部跟踪为深度监视的依赖关系
        if(this.deep){
            //teaverse方法递归每一个对象,将对象的每级属性收集为深度依赖项
            thraverse(value);
        }
        // 执行出栈
        popTarget();
        //调用实例cleanupDeps方法
        //清除所有依赖项
        this.cleanupDeps();
    }
    //返回观测数据
    return value;
}

下面是get()方法中的函数

var targetStack = [];
function pushTarget(_tasrget){
    if(Dep.target){
        targetStack.push(Dep.target);
    }
    Dep.target = _target;
}
function popTarget(){
    // pop() : 清除数组最后一位
    Dep.target = targetStack.pop();
}
function cleanupDeps(){
    let i = this.deps.length;
    while (i--){
        const dep = this.deps[i];
        if(!this.newDepIds.has(dep.id)){
            dep.removeSub(this);
        }
    }
    let tmp = this.depIds;
    this.depIds = this.newDepIds;
    this.newDepIds = tmp;
    this.newDepIds.clear();
    tmp = this.deps;
    this.deps = this.newDeps;
    this.newDeps = tmp;
    this.newDeps.length = 0;
}

7. traverse函数

export function traverse(val:any){
    _traverse(val,seenObjects);
    seenObjects.clear();
}

function _traverse(val:any,seen:SimpleSet){
    let i,keys;
    const isA = Array.isArray(val);  // val是对象中的所有值
    if((!isA && isObject(val)) || Obejct.isFrozen(val) || val instanceOf VNode){
        return;
    }
    if(val.__ob__){
        const depId = val.__ob__.dep.id; //id是通过dep函数添加上去的
        if(seen.has(depId)){
            return;
        }
        seen.add(depId);
    }
    if(isA){
        i = val.length;
        while(i--) _traverse(val[i].seen);
    }else{
        i = keys.length;
        while(i--) _traverse(val[keys[i]],seen);
    }
}

可能大家会对里面的一些方法不太了解, 下面是一些简单的介绍

  • Object.isFrozen() 与 instanceof()
function _traverse(val:any, senn:SimpleSet){
    let i,keys;
    const isA = Array.isArray(val);  // val是对象中的所有值
    if((!isA && isObject(val)) || Obejct.isFrozen(val) || val instanceOf VNode){
        // Object.isFrozen数据属性是指那些没有取值器(getter)或赋值器(setter)的属性。
        // 冻结对象是指那些不能添加新的属性, 不能修改已有属性的值, 不能删除已有属性, 
        // 以及不能修改已有属性的可枚举性、可配置性、可写性的对象。
        // 也就是说, 这个对象永远是不可变的。
        // instanceof 引用java的方法, 类似于typeof
        // 但typeof对于对象来说只能显示object, instanceof可以明确区分对象是否为某种特地的类型
        return;
    }
}
  • Object.keys()
if(val.__ob__){
    const depId = val.__ob__.dep.id; //id是通过dep函数添加上去的
    if(seen.has(depId)){
        return
    }
    seen.add(depId);
}
if(isA){
    i = val.length;
    while(i--) _traverse(val[i].seen);
}else{
    keys = Object.keys(val);
    // Object.keys()用于获取对象自身所有的可枚举的属性值,但不包括原型中的属性,
    // 然后返回一个由属性名组成的数组。注意它同for...in一样不能保证属性按对象原来的顺序输出
    
    // eg.
    // var colors = ['red','green','blue'];
    // colors.length = 10;
    // colors.push('yellow');
    // Array.prototype.demo = function(){ };
    // Object.keys(colors); //["0","1","2","10"];
    // var anObj = {a:'a', b:'b', d:'c'};
    // console.log(Object.keys(anObj));  //['a','b','d']
    
    i = keys.length;
    while(i--) _traverse(val[keys[i]],seen);
}

traverse函数思路分析

  1. traverse实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher,这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的 dep id 记录到 seenObjects,避免以后重复访问。

  2. 那么在执行了 traverse 后,我们再对 watch 的对象内部任何一个值做修改,也会调用 watcher 的回调函数了

总结:

综合上述, watch是在vue实例初始化时诞生的, 核心是通过get()函数获取监听值,然后做出对比的,当对比结果不一致后,则触发createWatcher里面的回调函数. 至于我们常用的deep/immediate属性,则是在get()/$watch以特定条件的形式展现出来, 从而实现其作用的.

最后一次编辑于  07-27  (未经腾讯允许,不得转载)
点赞 0
收藏
评论