手写简易版Vue源码之数据响应化的实现

当前,Vue和React已成为两大炙手可热的前端框架,这两个框架都算是业内一些最佳实践的集合体。其中,Vue最大的亮点和特色就是数据响应化,而React的特点则是单向数据流与jsx。

笔者近期正在研究Vue源码,在此过程中尝试实现一个简易版的Vue,而实现Vue的第一步便是解决数据响应化的问题。以下便是对Vue响应化的简易版实现。

数据响应的原理:

1、依赖收集:data通过Observer变成带有getter和setter方法的响应式对象,当外界通过Watcher获取数据时,会将该Watcher加入到Dep的依赖列表中,至此,就算完成了依赖收集

2、通知更新:当外界对已经响应化的对象,即data中的对象进行修改时,会触发setter方法,setter会通过Dep的通知方法,循环调用Dep依赖列表中Watcher的update方法通知外界更新视图或触发用户所给的监听回调

 

// kinerVue.js 简易版小程序入口
import Watcher from './Watcher.js';
import Observer,{set, del} from './Observer.js'
// 预期用法
// let vue = new KinerVue({
//     data(){
//         return {
//             name: "kiner",
//             userInfo: {
//                 age: 20
//             },
//             classify:['game','reading','running']
//         }
//     }
// });


// 数据响应的原理:
// 1、依赖收集:data通过Observer编程带有getter和setter方法的响应式对象,当外界通过Watcher获取数据时,会将该Watcher加入到Dep的依赖列表中,至此,就算完成了依赖收集
// 2、通知更新:当外界对已经响应化的对象,即data中的对象进行修改时,会触发setter方法,setter会通过Dep的通知方法,循环调用Dep依赖列表中Watcher的update方法通知外界更新视图或触发用户所给的监听回调


/**
 * 自定义简易版Vue
 */
class KinerVue{


    constructor(options){
        this.$options = options;
        this.$data = options.data.apply(this);

        // 将数据交给Observer,让Observer将这个数据变成响应式对象
        new Observer(this,this.$data);

        this.isVue = true;

        // test data start


        //测试$watch start
        let unWatchUserInfo = this.$watch("userInfo",(newVal,oldVal)=>{
            console.log(`$watch监听到[userInfo]发生改变,新值:`,newVal,`;旧值:`,oldVal);
        },{deep: false, immediate: true});
        this.$watch("userInfo.age",(newVal,oldVal)=>{
            console.log(`$watch监听到[userInfo.age]发生改变,新值:${newVal};旧值:${oldVal}`);
        });
        this.$watch("classify",function classifyWatcher(newVal,oldVal){
            console.log(`$watch监听到[classify]发生改变,新值:${newVal},;旧值:${oldVal}`);
        });
        this.$watch("friends",function classifyWatcher(newVal,oldVal){
            console.log(`$watch监听到[friends]发生改变,新值:${newVal},;旧值:${oldVal}`);
        });
        this.userInfo.age = 11;
        // 取消订阅,执行了这行代码之后$watch("userInfo",()=>{})将失效
        // unWatchUserInfo();
        this.userInfo.age = 20;

        //通过$set为数组设置值
        this.$set(this.classify,3,'999');
        this.$set(this.userInfo,'sex','男');
        this.$set(this.userInfo,'sex','女');

        //通过$delete删除后属性
        this.$delete(this.userInfo,"sex");

        console.log('sex:',this.userInfo)


        // console.log(this.classify);
        //测试$watch end


        // new Watcher(this,"name");
        // this.name;
        // new Watcher(this,"userInfo.age");
        // this.userInfo.age;
        // new Watcher(this,"classify");
        // this.classify;
        //
        // this.name = 'kanger';
        // console.log(this.name);
        // this.userInfo.age = 18;
        // console.log(this.userInfo.age);
        //
        this.classify.push(10);
        this.classify.splice(5,1,11);
        this.classify.unshift(12);
        this.classify.shift();
        this.classify.sort((a,b)=>a-b);
        this.classify.reverse();


        this.friends.push('zzz');
        this.friends.splice(2,1,'fff');
        this.friends.unshift('kkk');
        this.friends.sort((a,b)=>a-b);
        this.friends.reverse();
        this.friends.shift();
        // 由于未采用ES6的元编程能力,也就是proxy和reflect,因此无法监控类似arr[0]=xxxx和arr.length=0之类的数值变化,
        // 因此,在编码时要尽量避免这些写法,以免产生一些不可意料的问题
        //
        // this.classify[2] = 'working'; //错误用法
        // console.log(this.classify);


        // test data end
    }

    /**
     * 监听器,用于监听属性变化,并将新旧值传递回来,方便做一些拦截操作
     * @param exp       表达式或函数
     * @param cb        回调
     * @param options   配置项
     * @returns {Function}  取消观察的方法
     */
    $watch(exp,cb,options={immediate: true,deep: false}){
        let watcher = new Watcher(this,exp,cb,options);
        return ()=>{
            watcher.unWatch();
        };
    }

    /**
     * 设置属性,用来解决无法使用arr[0]=xxx,obj={} obj.name=xxx
     * @param target
     * @param key
     * @param value
     */
    $set(target,key,value){
        return set(target,key,value);
    }

    /**
     * 删除目标对象上的数据
     * @param target
     * @param key
     * @returns {undefined}
     */
    $delete(target,key){
        return del(target,key);
    }

}

export default KinerVue;
// utils.js 基础工具库,提供一些工具方法

/**
 * 判断对象是否支持__proto__属性
 * @type {boolean}
 */
export const hasProto = '__proto__' in {};
/**
 * 判断传递过来的对象是否是纯对象
 * @param obj
 * @returns {boolean}
 */
export const isPlainObject =  function(obj){
    let prototype;

    return Object.prototype.toString.call(obj) === '[object Object]'
        && (prototype = Object.getPrototypeOf(obj), prototype === null ||
        prototype === Object.getPrototypeOf({}))
};

/**
 * 判断是否为非空对象
 * @param obj
 * @returns {boolean}
 */
export const isObject = obj => (obj !== null && typeof obj === 'object');

/**
 * 显示警告消息
 * @param message
 */
export const warn = function (message) {
    console.warn(message);
};



/**
 * 定义不可枚举的属性
 * @param obj
 * @param key
 * @param value
 * @param enumerable 能否枚举
 */
export const def = function (obj,key,value,enumerable) {
    if(typeof obj === "object"){
        Object.defineProperty(obj,key,{
            value: value,
            configurable: true,
            enumerable: !!enumerable,
            writable: true
        });
    }
};

/**
 * 删除数组中的元素
 * @param arr
 * @param item
 * @returns {T[]}
 */
export const removeArrItem = function (arr, item) {
    const index = arr.indexOf(item);
    if(index!==-1){
        return arr.splice(index,1);
    }
};

/**
 * 根据表达式从目标对象中找到对应的值
 * e.g.
 *      若obj={userInfo:{userName}}
 *      exp="userInfo.userName"
 *
 * @param obj
 * @param exp
 * @returns {*}
 */
export const parseExp = function (exp) {

    return obj => {
        let reg = /[^\w.$]/;
        if(reg.test(exp)){
            return;
        }else{
            let subExp = exp.split('.');
            subExp.forEach(item=>{
                obj = obj[item];
            });
            return obj;
        }
    };
};

/**
 * 判断两个变量是否相等(但因为一个特殊情况,当a和b都等于NaN时,因为NaN===NaN输出为false)
 * @param a
 * @param b
 * @returns {boolean}
 */
export const isEqual = (a,b) => a===b||(a!==a&&b!==b);

/**
 * 将拦截器方法直接覆盖到目标对象的原型链上__proto__
 * @param obj
 * @param target
 * @returns {*}
 */
export const patchToProto = (obj,target) => obj.__proto__ = target;

/**
 * 直接在目标对象上定义不可枚举的属性
 * @param obj
 * @param arrayMethods
 * @param keys
 * @returns {*}
 */
export const copyArgument = (obj,arrayMethods,keys) => keys.forEach(key=>def(obj,key,arrayMethods[key]));

/**
 * 判断当前浏览器是否支持__proto__若支持,这直接将目标方法覆盖到__proto__上,否则,直接将方法定义在目标对象上
 * @param obj
 * @param src
 * @param keys
 * @returns {*}
 */
export const defProtoOrArgument = (obj,src,keys=Object.getOwnPropertyNames(src)) => hasProto ? patchToProto(obj,src) : copyArgument(obj,src,keys);

/**
 * 判断目标对象是否含有指定属性
 * @param obj
 * @param key
 * @returns {boolean}
 */
export const hasOwn = (obj,key) => obj.hasOwnProperty(key);

/**
 * 判断目标对象是否已经响应化
 * @param obj
 * @returns {boolean}
 */
export const hasOb = obj => hasOwn(obj,'__ob__');

/**
 * 判断传入参数类型是否为函数
 * @param fn
 * @returns {boolean}
 */
export const isFn = fn => typeof fn === "function";

/**
 * 判断所给参数是否是一个数组
 * @param arr
 * @returns {arg is Array<any>}
 */
export const isA = arr => Array.isArray(arr);

/**
 * 判断给定参数是否是合法的数组索引
 */
export const isValidArrayIndex = (val) => {
    const n = parseFloat(String(val));
    return n >= 0 && Math.floor(n) === n && isFinite(val)
};


export default {
    hasProto,
    isPlainObject,
    isObject,
    warn,
    def,
    removeArrItem,
    isEqual,
    parseExp,
    patchToProto,
    copyArgument,
    defProtoOrArgument,
    hasOwn,
    hasOb,
    isFn,
    isA
}
// Array.js 定义一些针对数组响应化时需要用到的辅助数据以及定义了
import {def} from "./utils.js";

// 数组原型,在对数组方法打补丁的时候,需要用到数组原型方法用于实现原本的数组操作
export const arrayProto = Array.prototype;

/**
 * 需要打补丁的数组方法,即会改变数组的方法
 * @type {string[]}
 */
export const needPatchArrayMethods = [
    "push",
    "pop",
    "unshift",
    "shift",
    "sort",
    "reverse",
    "splice"
];


// 根据数组原型创建一个新的基础数组对象,避免为数组方法打补丁的时候污染原始数组
export const arrayMethods = Object.create(arrayProto);


// 实现数组拦截器,通过这个拦截器实现拦截数组操作方法操作
needPatchArrayMethods.forEach(method=>{
    // 从数组原型中将原始方法取出
    const originalMethod = arrayProto[method];

    def(arrayMethods,method,function mutator(...args){
        // const oldVal = [...this];
        // 调用数组原始方法实现数组操作
        const res = originalMethod.apply(this,args);
        // 若当前数组已经是响应化后的数组,则将其Observe实例取出,用户后续通知更新操作
        const ob = this.__ob__;

        // 若执行的是会新增数组元素的方法,我们需要对新增的元素也进行响应化处理
        // 其中push和unshift接收的所有参数都是新增元素,因此直接将参数对象传递给defineReactiveForArray进行响应化处理
        // splice第2个之后的参数便为新增或替换的元素,因此将第2个之后的参数提取出来,传递给defineReactiveForArray进行响应化处理
        let inserted;
        switch (method){
            case "push":
            case "unshift":
                inserted = args;
                break;
            case "splice":
                inserted = args.splice(2);
                break;
        }
        inserted && ob.defineReactiveForArray(inserted);


        //通知依赖更新
        ob&&ob.dep.notify();
        // console.log(`---->触发了数组的${method}方法:新值:`,this,`;旧值:`,oldVal);
        return res;
    });
});
// Dep.js 依赖类,用于统一管理观察者,一旦依赖跟新,便可通过此类的notify方法通知其订阅的所有
// 观察者进行更新数据

import {removeArrItem} from "./utils.js";

let uid = 0;
/**
 * 用来管理所有的watcher
 */
class Dep {
    constructor(){
        // 订阅者列表
        this.subs = [];
        // 为每一个依赖定义一个唯一的id
        this.id = uid++;
    }

    /**
     * 触发添加依赖
     */
    depend(){
        //为实现取消订阅的功能,将订阅的方法放在watcher中,此处通过调用watcher的addDep将当前依赖加入到订阅列表,
        Dep.target&&Dep.target.addDep(this);
        // 初版实现,未实现取消订阅功能
        // Dep.target&&this.addDep(Dep.target);
    }

    /**
     * 添加订阅者
     * @param watcher 订阅者
     */
    addSub(watcher){
        // 为解决当调用数组的splice和sort方法时,会触发多次更新的问题,加入订阅时先看一下该依赖是否已经被添加
        if(this.subs.indexOf(watcher)<0){
            this.subs.push(watcher);
        }

    }

    /**
     * 从从订阅列表中移除订阅者
     * @param watcher
     */
    removeSub(watcher){
        removeArrItem(this.subs,watcher);
    }


    /**
     * 通知订阅者更新
     */
    notify(){
        this.subs.forEach(watcher=>{
            watcher.update()
        });
    }
}

export default Dep;
/**
 * Observer.js 数据响应化对象
 * Vue数据响应化的核心,Vue2.0时代通过Object.defineProperty方式进行数据响应化,而Vue3.0时代则采用Proxy和Reflect方式实现
 * 无论采用哪种方式,但其实现原理都是一样的,都是通过数据劫持的方式实现响应化
 */

import Dep from "./Dep.js";
import {isPlainObject, warn, isEqual,defProtoOrArgument, hasOb, def, isA, isValidArrayIndex, hasOwn} from "./utils.js";
import {arrayMethods} from "./Array.js";

class Observer {

    /**
     * 定义统一的操作方法,方便之后收集依赖和响应通知的统一操作
     * @param obj   待响应的对象
     * @param key   待响应的键值
     * @param value 待响应的值
     * @param childOb 子响应对象
     * @returns {*}
     */
    static baseHandler(obj, key, value,childOb) {
        //定义一个依赖对象,与data的key存在一一对应的关系
        const dep = new Dep();
        return {
            enumerable: true,
            configurable: true,
            get() {
                // Dep.target && dep.addDep(Dep.target);
                // 在访问对象属性时,将当前属性加入到依赖列表中
                dep.depend();

                // console.log('收集依赖',obj,key,value,childOb);
                // 用于收集数组对象的依赖
                childOb && childOb.dep.depend();

                // console.log(`获取${key}的值:${value}`);
                return value;
            },
            set(val) {
                // isEqual:原本的目的是为了判断新值val和旧值value相等的情况下,便直接退出,
                // 但因为一个特殊情况,当val和value都等于NaN时,因为NaN===NaN输出为false
                // 会让set方法继续往下执行,因此多加了一个(value!==value&&val!==val)进行拦截
                // 
                if (isEqual(val, value)) {
                    return;
                }
                // 由于旧值仍处于闭包当中,this.$data未释放的情况下,直接对value赋值可直接操作this.$data下对应键值下的数据,所以进行以下赋值操作
                value = val;
                // 通知依赖列表循环更新依赖
                dep.notify();

                // console.log(`设置${key}的值:${val}`);

            }
        }
    };


    constructor(vm, target) {
        this.$vm = vm;

        // 将跟数据target设置为已响应,以免重复创建示例
        def(target,'__ob__',this);

        //在此定义依赖收集对象,用来收集数组的依赖
        this.dep = new Dep();

        // 将目标变化变为响应式对象
        this.observer(target);

    }

    /**
     * 对传入的数据进行响应化处理
     * @param data
     */
    observer(data) {
        if (Array.isArray(data)) {//传过来的数据是否是数组
            defProtoOrArgument(data,arrayMethods);
            return this.defineReactiveForArray(data)
        } else if (isPlainObject(data)) {//传递过来的
            return this.defineReactiveForObject(data);
        } else {
            warn(`传递的数据必须是对象或数组,当前传递的值【${data}】类型为:${typeof data},因此无需响应化`);
        }
    }


    /**
     * 实现对象类型的响应化处理
     * @param obj
     */
    defineReactiveForObject(obj) {
        let keys = Object.keys(obj);

        keys.forEach(key => {
            this.defineReactive(obj, key, obj[key]);
            // 添加数据代理,将$data中的值代理到this,这样就可以直接通过this.xxx访问$data中的属性了
            this.proxyData(key);
        });
    }

    /**
     * 实现数组类型的响应化处理
     * @param data
     */
    defineReactiveForArray(data) {

        data.forEach(item=>this.createObserver(item));

    }

    /**
     * 将对象变为响应式对象,通过递归调用observer方法可以实现嵌套对象响应化
     * @param obj   带响应化对象
     * @param key   待响应的键值
     * @param value 待响应的值
     */
    defineReactive(obj, key, value) {

        let childOb = this.createObserver(value);

        Object.defineProperty(obj, key, Observer.baseHandler(obj, key, value,childOb));

    }


    /**
     * 判断目标数据是否已经响应化,如果响应化,则直接返回其响应化对象__ob__,佛则示例话一个响应化对象
     * @param data
     * @returns {*}
     */
    createObserver(data){
        let ob;
        if(hasOb(data)){//该对象已经响应化,直接获取
            ob = data.__ob__;
        }else{
            ob = new Observer(this.$vm,data);
        }
        return ob;
    }
   


    /**
     * 代理$data,将$data中的数据代理到vue实例中,便可直接通过this.xxx获取或设置值
     * @param key
     * @returns {*}
     */
    proxyData(key) {
        Object.defineProperty(this.$vm, key, {
            get() {
                return this.$data[key];
            },
            set(val) {
                this.$data[key] = val;
            }
        })
    }

}

/**
 * 为目标对象或数组增加设置/新增值
 * @param target
 * @param key
 * @param val
 * @returns {*}
 */
export const set = (target,key,val)=>{

    //如果target是数组且key是合法的数组索引,则将目标值加入到数组中
    if(isA(target) && isValidArrayIndex(key)){
        target.length = Math.max(target.length,key);
        target.splice(key,1,val);
        return val;
    }

    //如果key是target非原型链上的属性,说明该key已经是响应化对象了,无需重复响应化,直接修改对应的值即可
    if(key in target && !(key in Observer.prototype)){
        target[key] = val;
        return val;
    }

    //新增属性
    const ob = target.__ob__;

    //如果当前对象未被响应化,则直接设置目标值
    if(!ob){
        target[key] = val;
        return val;
    }

    // TODO 不能在跟对象this.$data和Vue示例上添加属性

    // 如果target是响应化对象,则通过Observer的defineRelative方法设置属性
    ob.defineReactive(target,key,val);
    ob.dep.notify();
    return val;

};

export const del = (target,key) => {
    //如果target是数组且key是合法的数组索引,则删除掉指定索引的数组项
    if(isA(target) && isValidArrayIndex(key)){
        target.splice(key,1);
        return;
    }
    //若target本身就不具有key属性,则无需删除,直接返回
    if(!hasOwn(target,key)) return;

    // TODO 不能在跟对象this.$data和Vue示例上删除属性

    const ob = target.__ob__;
    delete target[key];
    // 通知依赖更新
    ob && ob.dep.notify();
};


export default Observer;
// Watcher.js
// 它相当于是依赖Dep与具体的更新操作的一个中介,也可以理解为他是一个物流中转站,依赖就像是快递,具体更新操作就是快递的目的地,具体流程是这样的:
// 我们把快递(更新)交给快递代收点(Dep),当快递代收点(Dep)接收到快递之后,会有人来收集快递送到快递中转站(watcher),然后再由快递中转账再统一派发到不同的地址。
import Dep from './Dep.js';
import {parseExp, isObject,isFn} from "./utils.js";
import {arrayMethods} from "./Array.js";
import {traverse} from "./Traverse.js";

class Watcher {

    constructor(vm,expOrFn,cb=function(){},options={immediate: true,deep: false}){
        // 创建实例时,将当前实例对象指向Dep的静态属性target
        this.$vm = vm;
        // 需要坚挺的表达式或者是给定的函数(注:如为函数,则可在函数内使用到的响应化对象属性都会被观察,一旦任一属性值发生变化,都会触发cb回调通知)
        this.expOrFn = expOrFn;
        // 选项
        // options.immediate  true|false  代表是否在创建watcher实例时变直接运行表达式或函数获取结果
        // options.deep       true|false  代表是否进行深度观察,如果为true,会对指定表达式或对象下使用的属性的子属性进行递归观察操作
        //// e.g. data下的对象userInfo的结构是:userInfo:{friends:[{name:'kiner'},{name:'kanger'}],bankInfo:{bankCardNum: 'xxxxxxx'}}
        //// 那么,如果我们要进行深度观察,则如:this.$watch("userInfo",()=>{},{deep:true})
        //// 此后,一旦userInfo下面的任一属性,包括子对象、数组中的值发生改变,上述的$watch都能够观察得到
        this.options = options;

        // 若给出的是函数,则直接将其赋值给gutter
        if(isFn(expOrFn)){
            this.gutter = expOrFn;
        }else{
            // 若给出的是一个如:userInfo.name或age之类的表达式,则通过parseExp这个高阶函数将表达式进行一定的处理并赋值给gutter
            // 使我们可以直接通过this.gutter.call(this.$vm,this.$vm);的方式直接获得表达式对应的结果
            this.gutter = parseExp(expOrFn);
        }

        // 观察者通知的回调函数
        this.cb = cb;


        // 为实现取消订阅功能,需要知道watcher都订阅了哪些依赖,在取消订阅时,秩序把对应的依赖从依赖列表移除即可
        // 为方便订阅,将依赖列表从Dep移到watcher
        this.deps = [];

        // 为了标志依赖的唯一性,定义一个不可重复的Set用于存储依赖的id
        this.depIds = new Set();

        // 如果指定immediate=true则在实例化时离开触发get获取目标值
        if(options.immediate){
            this.value = this.get();
        }


    }

    /**
     * 尝试通过表达式或者所给方法获取目标值
     * @returns {*}
     */
    get(){
        Dep.target = this;//指定快递代收点所属的中转站,这样才能够将快递精确的从代收点送到中转站
        //根据给定的表达式或函数直接或取目标值,与此同时,因为触发了get,会将Dep.target添加到依赖列表当中
        let value = this.gutter.call(this.$vm,this.$vm);
        this.sourceValue = value;

        // 若需要观察对象系所有子对象的变化(注:此步骤必须放在`Dep.target = undefined;`之前,因为递归收集子对象依赖时仍需要使用到Dep.target)
        if(this.options.deep){
            traverse(value);
        }


        //尝试解决当value为数组或对象时,newVal和oldVal恒等问题(注:此步骤是因为个人开发原因需要获取对象或数组的新旧值,为方便操作,尝试性实现,Vue官方并无此步骤)
        if(value.__proto__===arrayMethods){
            value = [...value];
        }else if(isObject(value)){
            value = {...value}
        }

        // 加入依赖列表之后释放target
        Dep.target = undefined;
        return value;
    }

    // 中转站已经收到快递了,准备派送,通知各位快递小哥过来拿各自负责区域(视图中的表达式或$watch中监听的方法)的快递进行派送
    update(){
        // 接收到更新通知时,触发get方法获取改表达式最新的值
        const value = this.get();
        // vue源码中:如果value是数组/对象时,我们通过$watch((newVal,oldVal)=>{})获取到的newVal和oldVal其实是始终相等的,因为他们都是东一个对象的引用
        if(this.value!==value||isObject(value)){

            const oldVal = this.value;
            this.value = value;
            // 将新旧值传递给回调函数,即完成$watch('xxxxx',function(newVal,oldVal){})的通知
            this.cb.call(this.$vm,value,oldVal);
        }

        // console.log(`属性${this.expOrFn}发生了变化`);
    }

    /**
     * 添加依赖并经自己订阅到依赖当中
     * @param dep
     */
    addDep(dep){
        const depId = dep.id;
        // 判断依赖是否已经在依赖列表中,若不存在,则添加依赖
        if(!this.depIds.has(depId)){
            this.deps.push(dep);
            this.depIds.add(depId);
            // 为添加的依赖订阅观察者
            dep.addSub(this);
        }

    }

    /**
     * 取消观察,移除依赖列表中所有的当前观察者
     */
    unWatch(){
        let len = this.deps.length;
        while (len--){
            this.deps[len].removeSub(this);
        }
    }

}

export default Watcher;
// Traverse.js 通过traverse递归访问指定对象,通过触发getter的方式实现依赖收集
import {isA,isObject,hasOb} from "./utils.js";

// 用于存储依赖id
const depIds = new Set();

// 通过这个方法访问一下给定目标对象的子对象,从而触发依赖通知
export const traverse = (val) => {
    _traverse(val,depIds);
    depIds.clear();
};

function _traverse(val,depIds){
    let len,keys;
    // 所传对象如果类型不是非冻结对象或数组,就直接终止
    if((!isA(val) && !isObject(val)) || Object.isFrozen(val)){
        return;
    }
    // 判断当前对象是否已经是响应化对象
    if(hasOb(val)){
        const  depId = val.__ob__.dep.id;
        if(depIds.has(depId)){//已经访问过了,直接终止
            return;
        }
        //若未访问过,则将依赖id加入到depIds中
        depIds.add(depId);
    }

    if(isA(val)){//如果是数组,则循环访问其子项并递归访问
        len = val.length;
        while (len--) _traverse(val,depIds);
    }else{//循环对象下的所有属性并递归访问
        keys = Object.keys(val);
        len = keys.length;
        while (len--) _traverse(val,depIds);
    }
}

以上代码已实现功能:

  1. 数据响应化-Observe.js(Array.js-数组响应化的一些相关处理)
  2. 数据观察者-Watcher.js(Traverse.js-通过traverse递归访问指定对象,通过触发getter的方式实现依赖收集)
  3. 依赖管理者-Dep.js
  4. 工具方法$watch-观察属性变化的方法、$set-为对象添加属性或者为数组添加子项,并通知依赖更新、$delete-删除对象属性或删除数组子项并通知依赖更新

以后将陆续会尝试实现:

  1. 虚拟Dom(VNode)
  2. 编译器
  3. Vue生命周期钩子、工具方法、全局Api实现
  4. 指令的解析
  5. 过滤器的实现
  6. 行业最佳实践学习(Vue-Router、Vuex、Element-UI、Element-Admin、Vant)

文章将根据本人对Vue及其相关最佳实践的学习进度不断更新,如有不对,欢迎指正,谢谢!

PS:如果对Vue3(即vue-next)感兴趣的同学,可以看一下本人撰写的另一篇文章 Vue3(Vue-next)响应化实现剖析,这里简单的实现了使用es6元编程能力(proxy和reflect)实现的数据响应化原理。

发布了32 篇原创文章 · 获赞 16 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/u010651383/article/details/103952529