zoukankan      html  css  js  c++  java
  • vue的双向绑定

    撸一个vue的双向绑定

     

    1、前言

    说起双向绑定可能大家都会说:Vue内部通过Object.defineProperty方法属性拦截的方式,把data对象里每个数据的读写转化成getter/setter,当数据变化时通知视图更新。虽然一句话把大概原理概括了,但是其内部的实现方式还是值得深究的,本文就以从简入繁的形式给大家撸一遍,让大家了解双向绑定的技术细节。

    2、来一个简单的版本

    让我们的数据变得可观测,实现原理不难,利用Object.defineProperty重新定义对象的属性描述符。

     /**
        * 把一个对象的每一项都转化成可观测对象
        * @param { Object } obj 对象
        */
        function observable(obj) {
            if (!obj || typeof obj !== 'object') {
                return;
            }
            let keys = Object.keys(obj);
            keys.forEach((key) => {
                defineReactive(obj, key, obj[key])
            })
            return obj;
        }
        /**
        * 使一个对象转化成可观测对象
        * @param { Object } obj 对象
        * @param { String } key 对象的key
        * @param { Any } val 对象的某个key的值
        */
        function defineReactive(obj, key, val) {
            Object.defineProperty(obj, key, {
                get() {
                    console.log(`${key}属性被读取了`);
                    return val;
                },
                set(newVal) {
                    console.log(`${key}属性被修改了`);
                    val = newVal;
                }
            })
        }
        let car = observable({
            'brand': 'BMW',
            'price': 3000
        })
        //测试
        console.log(car.brand);
    

    3、一步一步实现一个观察者模式的双向绑定

    先给一张思维导向图吧(图盗的,链接:https://www.cnblogs.com/libin-1/p/6893712.html),本文章不涉及Compile部分。
    这张图我就不解释,我们先跟着一步一步的把代码撸出来,再回头来看这张图,问题不大。

    建议在读之前一定要了解观察者模式和发布订阅模式以及其区别,一篇简单的文章总结了一下两种模式的区别(链接:https://www.cnblogs.com/chenlei987/p/10504956.html),Vue的双向绑定使用的就是观察者模式,其中Dep对象就是观察者的目标对象,而Watcher就是观察者,然后等待Dep对象的通知更新的,其中update方法是由watcher自己管理的,并非如发布订阅模式由目标对象去管理,在观察者模式中,目标对象管理的订阅者列表应该是Watcher本身,而不是事件/订阅主题。

    3.1、声明一个Vue类,并将data里面的数据代理到Vue实例上面。

    var Vue = (function (){
        class Vue{
                constructor (options = {}){
    
                    //简化处理
                    this.$options = options;
    
                    let data = (this._data = 
                        typeof this.$options.data == 'function' ? 
                            this.$options.data() 
                            : 
                            this.$options.data);
                    Object.keys(data).forEach(key =>{ this._proxy(key) });
    
                    // 监听数据
                    //observe(data);
                }
                _proxy (key){
                    //用this这个对象去代理 this._data这个对象里面的key
                    Object.defineProperty(this, key, {
                        configurable: true,
                        enumerable: true,
                        set: (val) => {
                            this._data[key] = val
                        },
                        get: () =>{
                            return this._data[key]
                        }
                    })
                }
                
            }
            return Vue;
    }
    let VM = new Vue({
        data (){
            return {
                a: 1,
                arr: [1,2,3,4,5,6]
            }
        },
    });
    //说明 _proxy代理成功了
    console.log(VM.a);
    VM.a = 2;
    console.log(VM.a);
    

    3.2、让data里面的数据变得可观测,开启observe之旅

    注:下面我所说的"data里面"就是指vue实例的data属性。
    上面代码Vue类的constructor里面我注释了一行代码,下面我取消注释,并且开始定义observe函数

    // 监听数据
     observe(data);

    在定义observe方法之前,首先明白我们observe要做什么?
    实参是data数据,我们要遍历整个data数据的key,为data数据的每一个key都用Object.defineProperty去重新定义它的 getter和setter函数,从而使其可观测。

    class Observer{
                constructor (value){
                    this.value = value;
                    this.walk(value);
                }
                // 遍历属性值并监听
                walk(value) {
                    Object.keys(value).forEach(key => this.convert(key, value[key]));
                }
                // 执行监听的具体方法
                convert(key, val) {
                    defineReactive(this.value, key, val);
                }
            }
            function defineReactive(obj, key, val) {
                const dep = new Dep();
                // 给当前属性的值添加监听
                let chlidOb = observe(val);
                Object.defineProperty(obj, key, {
                    enumerable: true,
                    configurable: true,
                    get: () => {
                      
                      //do something
                      //  if (Dep.target) {
                        //    dep.depend();
                        //}
                        return val;
                    },
                    set: newVal => {
                        if (val === newVal) return;
                        val = newVal;
                        
                        //do something
                        // 对新值进行监听
                        //chlidOb = observe(newVal);
                        // 通知所有订阅者,数值被改变了
                        //dep.notify();
                    },
                });
            }
    
            function observe(value) {
                 // 当值不存在,或者不是复杂数据类型时,不再需要继续深入监听
                if (!value || typeof value !== 'object') {
                    return;
                }
                return new Observer(value);
            }
    

    看到在get和set函数里面的do something了吗,可以理解为在data里面的每个key的设置和获取都被我们截取到了,在每个key的设置和获取时我们可以干些事情了。比如更数据对应的DOM。
    要做什么呢?
    get函数: 从思维图图1可以看出需要把当前的Watcher添加进Dep对象,等待数据更新,调用回调。
    set函数: 数据更新,Dep对象通知所有订阅的watcher更新,调用回调,更新视图。

    3.3、Watcher

    先声明一个Watcher类,用于添加进Dep对象并通知更新视图使用。

     let uid = 0;
            class Watcher {
                constructor(vm, expOrFn, cb) {
                    // 设置id,用于区分新Watcher和只改变属性值后新产生的Watcher
                    this.id = uid++;
    
                    this.vm = vm; // 被订阅的数据一定来自于当前Vue实例
                    this.cb = cb; // 当数据更新时想要做的事情
                    this.expOrFn = expOrFn; // 被订阅的数据
                    this.val = this.get(); // 维护更新之前的数据
                }
                // 对外暴露的接口,用于在订阅的数据被更新时,由订阅者管理员(Dep)调用
                update() {
                    this.run();
                }
                addDep(dep) {
                    // 如果在depIds的hash中没有当前的id,可以判断是新Watcher,因此可以添加到dep的数组中储存
                    // 此判断是避免同id的Watcher被多次储存
                    //这里要是不限制重复,你会发现在响应的过程中,Dep实例下的subs会成倍的增加watcher。多输入几个字浏览器就卡死了。
                    if (!dep.depIds.hasOwnProperty(this.id)) {
                        dep.addSubs(this);
                        dep.depIds[this.id] = dep;
                    }
                }
                run() {
                    const val = this.get();
                    if (val !== this.val) {
                        this.val = val;
                        this.cb.call(this.vm, val);
                    }
                }
                get() {
                    // 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者
                    Dep.target = this;
                    //注意:在这里获取该属性 从而就触发了defineProperty的get方法,该watcher已经进入Dep的subs队列了
                    const val = this.vm._data[this.expOrFn]; 
                    
                    //初始化执行一遍回调
                    this.cb.call(this.vm, val);
    
                    //  置空,用于下一个Watcher使用
                    Dep.target = null;
                    return val;
                }
            }
    

    上面代码我们先从constructor看起,接受三个参数,vm当前的vue实例,expOrFn实例化时该watcher实例所 代表/处理 的"data里面"(‘data里面’上面有解释,这里提醒一下)的哪个值,cb,回调函数,也就是当数据更新后需要做什么(自然是更新DOM咯)。
    然后在constructor里面还调用了 this.get()。详细看一下get函数的定义,两行代码需要注意:

    // 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者
    Dep.target = this;
    //注意:在这里获取该属性 从而就触发了defineProperty的get方法,该watcher已经进入Dep的subs队列了
    const val = this.vm._data[this.expOrFn]; 
    

    Dep.target = this;确定了当前的活动的watcher实例,Dep.target我们可以认为它是一个全局变量,用于存放当前活动的watcher实例。
    const val = this.vm._data[this.expOrFn];获取数据,这句话其实就已经触发了其自身的getter方法(这点要注意,不然你连流程都理解不通)。
    进入了getter方法,也就把当前活动的实例的watcher添加进dep对象等待更新。
    添加进Dep对象后,置空,用于下一个Watcher使用 Dep.target = null;

    3.4、Dep

    一直在说dep对象,我们一定要知道dep对象就是观察者模式里面的目标对象,用于存放watcher和负责通知更新的。
    下面来定义一个Dep对象,放到class Watcher前面。 注意Dep的作用范围.

    class Dep{
                constructor (){
                    this.depIds = {}; // hash储存订阅者的id,避免重复的订阅者
                    //订阅者列表  watcher实例列表
                    this.subs = [];
                }
                depend (){
                    Dep.target.addDep(this);//相当于调用this.addSubs 将 watcher实例添加进订阅列表 等待通知更新
                    //本来按照我们的理解,在denpend里面是需要将watcher添加进 Dep对象, 等待通知更新的,所以应该调用 this.addSubs(Dep.target)
                    //但是由于需要解耦 所以 先调用 watcher的addDep 在addDep中调用Dep实例的addSubs
                    //简化理解就是 将 watcher实例添加进订阅列表 等待通知更新
                }
                addSubs (sub) {
                    //这里的sub肯定是watcher实例
                    this.subs.push(sub);
                }
                notify (){
                    //监听到值的变化,通知所有订阅者watcher更新
                    this.subs.forEach((sub) =>{
                        sub.update();
                    });
                }
            }
             Dep.target = null;//存储当前活动的watcher
    

    再改改defineReactive,把注释打开

    function defineReactive(obj, key, val) {
                const dep = new Dep();
                // 给当前属性的值添加监听
                let chlidOb = observe(val);
                Object.defineProperty(obj, key, {
                    enumerable: true,
                    configurable: true,
                    get: () => {
                        // 如果Dep类存在target属性,将其添加到dep实例的subs数组中
                        // target指向一个Watcher实例,每个Watcher都是一个订阅者
                        // Watcher实例在实例化过程中,会读取data中的某个属性,从而触发当前get方法
                        if (Dep.target) {
                            dep.depend();
                        }
                        return val;
                    },
                    set: newVal => {
                        if (val === newVal) return;
                        val = newVal;
                        // 对新值进行监听
                        chlidOb = observe(newVal);
                        // 通知所有订阅者,数值被改变了
                        dep.notify();
                    },
                });
            }
    

    然后起一个watcher来监听

    3.5、让数据响应起来

    先给Vue暴露一个方法 $watcher 可以调用实例化Watcher。

    class Vue{
                constructor (options = {}){
    
                    //简化处理
                    this.$options = options;
    
                    let data = (this._data = 
                        typeof this.$options.data == 'function' ? 
                            this.$options.data() 
                            : 
                            this.$options.data);
                    Object.keys(data).forEach(key =>{ this._proxy(key) });
    
                    // 监听数据
                    observe(data);
                }
                // 对外暴露调用订阅者的接口,内部主要在指令中使用订阅者
                $watch(expOrFn, cb) {
                    //property需要监听的属性  cb在监听到更新后的回调
                    new Watcher(this, expOrFn, cb);
                }
                _proxy (key){
                    //用this这个对象去代理 this._data这个对象里面的key
                    Object.defineProperty(this, key, {
                        configurable: true,
                        enumerable: true,
                        set: (val) => {
                            this._data[key] = val
                        },
                        get: () =>{
                            return this._data[key]
                        }
                    })
                }
            }
    

    3.6、测试: 声明一个实例

    html部分

     <h3>Vue双向绑定</h3>
        <input type="text" id="input">
        <p id="react"></p>
        <h3>Vue数组双向绑定</h3>
        <input type="text" id="arr-input">
        <p id="arr-reat"></p>
    
    let reactElement = document.querySelector("#react");
        let input = document.getElementById('input');
        input.addEventListener('keyup', function (e) {
            VM.a = e.target.value;
        });
    
        VM.$watch('a', val => reactElement.innerHTML = val); //监听属性 a 当a发生改变时
    
    
        //数组的响应并不能实现
        let arrReactElement = document.querySelector("#arr-reat");
        let arrInput = document.getElementById('arr-input');
        arrInput.addEventListener('keyup', function (e) {
            VM.arr.push(e.target.value);
            console.log(VM.arr);
        });
        VM.$watch('arr', val => arrReactElement.innerHTML = val); //监听属性 a 当a并没有发生改变时
    

    VM.$watch就可以实例化一个watcher,从而去劫持data里面某个属性的改变,在改变时调用回调函数。
    数组的改变并没有实现。上面的代码见https://gitee.com/cchennlleii/MyTest/blob/master/vueReactive/vue-reactive%E7%AE%80%E5%8D%95%E5%AE%9E%E7%8E%B0.html

    4、对数组的支持

    在说这个之前我们先去看一看vue官网对于数组更新检测的说明,链接:https://cn.vuejs.org/v2/guide/list.html#%E6%95%B0%E7%BB%84%E6%9B%B4%E6%96%B0%E6%A3%80%E6%B5%8B


    总的来说,对于数组支持更新的只是数组原型上的方法,对于vm.items[index] = newValue是不支持的。
    其实Object.defineProperty对于数组都是不支持的,根据消息vue3.0用的proxy对于数组得到了完美的支持,但是兼容性不怎么样。
    既然vue实现了对数组原型方法的支持,那么我们也来让我们的例子对数组方法也支持吧。
    原理不难,vue对于所有的数组原型方法都写了一层hack,让其支持更新。那么下面我们就一步一步来实现。

    4.1、准备一套数组原型方法的hack

    /**
            * Define a expOrFn.
            */
            function def(obj, key, val, enumerable) {
                Object.defineProperty(obj, key, {
                    value: val,
                    enumerable: !!enumerable,
                    writable: true,
                    configurable: true
                });
            }
    
            //数组改变的监听
            var arrayProto = Array.prototype;
            var arrayMethods = Object.create(arrayProto);
            var methodsToPatch = [
                'push',
                'pop',
                'shift',
                'unshift',
                'splice',
                'sort',
                'reverse'
            ];
            /**
            * Intercept mutating methods and emit events
            */
            methodsToPatch.forEach(function (method) {
                // cache original method
                var original = arrayProto[method];
                def(arrayMethods, method, function mutator() {
                    var args = [], len = arguments.length;
                    while (len--) args[len] = arguments[len];
    
                    var result = original.apply(this, args);
                    var ob = this.__ob__;
                    var 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(); //调用该数组下的 __ob__.dep 详细可见class Observer的constructor里的注释
                    return result
                });
            });
    

    上面代码准备了一个arrayMethods的对象,它继承自Array.prototype,并且对methodsToPatch里面的方法进行了改写,后面我们会把arrayMethods这个对象挂到"data里面"每个数组下,让该数组调用数组原生方法,比如[].push其实调用的是arrayMethods里面被改写的方法,从而在该数组改变时获取到该数组的更新。
    下面开始挂载arrayMethods对象,在挂载我之前我们看到有一个this.__ob__属性,这里的this指向要观测的数组。这个__ob__就是前面的observe对象,并且每个observe下面还有一个dep对象。下面我们来理清楚这层关系。

    class Observer{
        constructor (value){
            this.value = value;
    
            //下面两行代码虽然很简单,但是我们需要从这里理清楚关系
            //假如 有数据如 {a: [1,2,3], b: 1},  然后调用oberve(vm.a),vm当前vue实例
            //会自动挂载 __ob__ 和 __ob__.dep
            // 那么对数组a进行oberserve的对象就是a.__ob__, 它所对应的dep对象就是 a.__ob__.dep
            //详细使用可以在对数组的方法进行hack的时候 使用到
            def(value, '__ob__', this);//让被监听的数据都带上一个不可枚举的属性 __ob__ 代表observe对象
            this.dep = new Dep();//首先每个oberserve实例下有一个dep对象
            
            
            //在这里处理数组
            if (Array.isArray(value)){
                //调用数组的hack方法, 让数组也能被监听  arrayMethods
                var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
                for (var i = 0, l = arrayKeys.length; i < l; i++) {
                    var key = arrayKeys[i];
                    def(value, key, arrayMethods[key]);
                }
            }   
            else{
                //对象 遍历key  添加监听
                this.walk(value);
            }
        }
        //Observer的其他方法
        //...
    }
    

    上面代码首先给每个值挂载__ob__属性(不可枚举),然后给每个Obeserve对象挂载Dep对象。然后根据value的类型,如果是数组就会挂载arrayMethods方法。
    现在我们来理清数组在哪里依赖收集,在哪里通知更新的。
    在对数组hack的方法里面(上上一段代码)有一段ob.dep.notify(); 这里通知更新,所以依赖收集也一定要收集到value.__ob__.dep对象里面,两个dep对象应该是相同的,下面我们来看看依赖收集写在哪里的。

    function defineReactive(obj, key, val) {
                const dep = new Dep();
                // 给当前属性的值添加监听
                let childOb = observe(val);
                Object.defineProperty(obj, key, {
                    enumerable: true,
                    configurable: true,
                    get: () => {
                        // 如果Dep类存在target属性,将其添加到dep实例的subs数组中
                        // target指向一个Watcher实例,每个Watcher都是一个订阅者
                        // Watcher实例在实例化过程中,会读取data中的某个属性,从而触发当前get方法
                        if (Dep.target) {
                            dep.depend();
                            if (childOb) {
                                childOb.dep.depend();
                                if (Array.isArray(val)) {
                                    dependArray(val);
                                }
                            }
                        }
                        return val;
                    },
                    set: newVal => {
                        if (val === newVal) return;
                        val = newVal;
                        // 对新值进行监听
                        childOb = observe(newVal);
                        // 通知所有订阅者,数值被改变了
                        dep.notify();
                    },
                });
            }
            function dependArray(value) {
                for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
                    e = value[i];
                    e && e.__ob__ && e.__ob__.dep.depend();
                    if (Array.isArray(e)) {
                        dependArray(e);
                    }
                }
            }
    

    数组虽然在Object.defineProperty里面set方法无法响应,但是get方法是没有问题的,所以在数组get的时候,判断val如果是array,会调用value.__ob__.dep.depend进行依赖收集。与上面依赖通知使用了同意个dep对象,也就是挂载在自身的__ob__.dep。
    写到这里我们就完全实现对数组原生方法的支持了。
    下面看一下效果 代码地址:https://gitee.com/cchennlleii/MyTest/blob/master/vueReactive/vue-reactive%E5%AF%B9%E6%95%B0%E7%BB%84%E7%9A%84%E6%94%AF%E6%8C%81.html

    4.2测试代码

    html部分

    <h3>Vue双向绑定</h3>
        <input type="text" id="input">
        <p id="react"></p>
        <h3>Vue数组双向绑定</h3>
        <input type="text" id="arr-input">
        <p id="arr-reat"></p>
        <h3>Vue对nextTick实现</h3>
        <button id="addBtn">加100000次</button>
        <p id="react-tick"></p>
    
    let reactElement = document.querySelector("#react");
        let input = document.getElementById('input');
        input.addEventListener('keyup', function (e) {
            VM.a = e.target.value;
        });
    
        VM.$watch('a', val => reactElement.innerHTML = val); //监听属性 a 当a发生改变时
    
    
        //数组的响应并能实现
        let arrReactElement = document.querySelector("#arr-reat");
        let arrInput = document.getElementById('arr-input');
        arrInput.addEventListener('keyup', function (e) {
            VM.arr.push(e.target.value);
            console.log(VM.arr);
        });
        VM.$watch('arr', val => arrReactElement.innerHTML = val); //监听属性 a 当a发生改变时
    
    
        let reactTick = document.querySelector("#react-tick");
        VM.$watch('tickData', val => {
            console.log(val);
            reactTick.innerHTML = val;
        }); //监听属性 a 当a发生改变时
        document.querySelector('#addBtn').addEventListener('click', function () {
            for (let i = 0; i < 100000; i++) {
                VM.tickData = i;
            }
        }, false)
    

    效果:

    5、对nextTick的支持

    vue官网对nextTick的解释:

    nextTick如果自己实现就是在下一个envet loop执行,不在本次同步任务中执行。
    自己实现一个简单的:

    //nextTick的实现
    let callbacks = [];
    let pending = false;
    
    function nextTick(cb) {
        callbacks.push(cb);
        if (!pending) {
            pending = true;
            setTimeout(flushCallbacks, 0);
        }
    }
    function flushCallbacks() {
        pending = false;
        const copies = callbacks.slice(0);
        callbacks.length = 0;
        for (let i = 0; i < copies.length; i++) {
            copies[i]();
        }
    }
    

    简单理解: 在本次event loop中收集cb(任务),放到下一个event loop去执行。 关于不知道event loop的可以参考这篇文章:https://www.cnblogs.com/chenlei987/p/10479433.html,我总结的很简单。我参考的http://www.ruanyifeng.com/blog/2014/10/event-loop.html。
    在理解event loop的同时也需要同时了解 microtask和macrotask的区别。
    好了言归正传,在vue的'data里面'某个属性发生了改变,并被观测到后,调用了watcher.update,并不会立即调用watcher.run去更新视图,它会经过nextTick之后再更新视图,说起来有点牵强。
    还是第四部=步的代码,没有实现对nextTick的优化。
    代码:

    <h3>Vue双向绑定</h3>
        <input type="text" id="input">
        <p id="react"></p>
        <h3>Vue数组双向绑定</h3>
        <input type="text" id="arr-input">
        <p id="arr-reat"></p>
        <h3>Vue对nextTick实现</h3>
        <button id="addBtn">加1000次</button>
        <p id="react-tick"></p>
    
    let reactTick = document.querySelector("#react-tick");
        VM.$watch('tickData', val => {
            console.log(val);
            reactTick.innerHTML = val;
        }); //监听属性 a 当a发生改变时
        document.querySelector('#addBtn').addEventListener('click', function () {
            for (let i = 0; i < 1000; i++) {
                VM.tickData = i;
            }
        }, false)
    

    效果是这样的:

    现在的效果是VM.tickData加1000次,那么cb(回调)就会调用1000次,这样是非常影响性能的,我们想要的效果是无论VM.tickData在本次event loop加多少次,都不会触发回调,只需要在VM.tickData加完之后,触发一次最终的cb(回调)就ok了。
    下面我们就来实现这种优化,代码不多。

    //nextTick的实现
                let callbacks = [];
                let pending = false;
    
                function nextTick(cb) {
                    callbacks.push(cb);
                    if (!pending) {
                        pending = true;
                        setTimeout(flushCallbacks, 0);
                    }
                }
                function flushCallbacks() {
                    pending = false;
                    const copies = callbacks.slice(0);
                    callbacks.length = 0;
                    for (let i = 0; i < copies.length; i++) {
                        copies[i]();
                    }
                }
                
                let has = {};
                let queue = [];
                let waiting = false;
                function queueWatcher(watcher) {
                    const id = watcher.id;
                    if (has[id] == null) {
                        has[id] = true;
                        queue.push(watcher);
    
                        if (!waiting) {
                            waiting = true;
                            nextTick(flushSchedulerQueue);
                        }
                    }
                }
                function flushSchedulerQueue() {
                    let watcher, id;
    
                    for (index = 0; index < queue.length; index++) {
                        watcher = queue[index];
                        id = watcher.id;
                        has[id] = null;
                        watcher.run();
                    }
    
                    waiting = false;
                }
    

    然后更改Watcher里面的update方法,并不直接调用watcher.run,而是经过queueWatcher控制

    update() {
        queueWatcher(this);
        // this.run();
    }

    代码地址:https://gitee.com/cchennlleii/MyTest/blob/master/vueReactive/vue-reactive%E5%AF%B9nextTck%E7%9A%84%E6%94%AF%E6%8C%81.html

    6、总结

    如果面试官问我关于双向绑定的问题,从这三个方面去回答,Object.definproperty,观察者模式,nextTick,当然,你需要把这三个点联系起来去描述,相信我你把上面的看懂了,联系起来完全没问题的,你是最棒的!

    7、本文参考:

    https://codepen.io/xiaomuzhu/pen/jxBRgj/
    https://www.jianshu.com/p/2df6dcddb0d7

  • 相关阅读:
    Python 模块 itertools
    Python 字符串的encode与decode
    python 模块 hashlib(提供多个不同的加密算法)
    暴力尝试安卓gesture.key
    hdu 1300 Pearls(DP)
    hdu 1232 畅通工程(并查集)
    hdu 1856 More is better(并查集)
    hdu 1198 Farm Irrigation(并查集)
    hdu 3635 Dragon Balls(并查集)
    hdu 3038 How Many Answers Are Wrong(并查集)
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/11071912.html
Copyright © 2011-2022 走看看