zoukankan      html  css  js  c++  java
  • AngularJS源码解析3:RootScope的创建过程

    RootScopeProvider简介

    RootScopeProvider是angularjs里面比较活跃的一个provider。它主要用来生成实例rootScope,它代表angularjs应用的根作用域。我们可以把它看成MVVM模式中的VM。

    源代码如下:

    function $RootScopeProvider(){
            var TTL = 10;
            var $rootScopeMinErr = minErr('$rootScope');
            var lastDirtyWatch = null;
    
            this.digestTtl = function(value) {
                if (arguments.length) {
                    TTL = value;
                }
                return TTL;
            };
            this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser',
                function( $injector,   $exceptionHandler,   $parse,   $browser) {
                    .......
                }];
        }

    在$RootScopeProvider构造函数中,有一个$get属性,它的值是一个数组,数组的最后的一项是一个函数function( $injector,   $exceptionHandler,   $parse,   $browser) ,所有的代码都在这个方法中。angularJS内置的provider都有一个$get属性,此属性值主要是定义实例化provider对象的函数体,实例化时,会通过instanceinjector.invoke来调用。在$get属性上面还有一个digestTtl属性,这个属性是用来修改angularJS默认的dirty check次数,angularJS默认是10次。接下来,我们来看下$get属性值中的实例化

    $RootScopeProvider的函数体。

              function Scope() {
                        this.$id = nextUid();
                        this.$$phase = this.$parent = this.$$watchers =
                            this.$$nextSibling = this.$$prevSibling =
                                this.$$childHead = this.$$childTail = null;
                        this['this'] = this.$root =  this;
                        this.$$destroyed = false;
                        this.$$asyncQueue = [];
                        this.$$postDigestQueue = [];
                        this.$$listeners = {};
                        this.$$listenerCount = {};
                        this.$$isolateBindings = {};
                    }
                    Scope.prototype = {
                        constructor: Scope,
                        $new: function(isolate) {
                            .....
                        },
                        $watch: function(watchExp, listener, objectEquality) {
                           ......
                        },
                        $watchCollection: function(obj, listener) {
                            var self = this;
                            var oldValue;
                            var newValue;
                            var changeDetected = 0;
                            var objGetter = $parse(obj);
                            var internalArray = [];
                            var internalObject = {};
                            var oldLength = 0;
                            function $watchCollectionWatch() {
                                newValue = objGetter(self);
                                var newLength, key;
                                if (!isObject(newValue)) {
                                    if (oldValue !== newValue) {
                                        oldValue = newValue;
                                        changeDetected++;
                                    }
                                } else if (isArrayLike(newValue)) {
                                    if (oldValue !== internalArray) {
                                        oldValue = internalArray;
                                        oldLength = oldValue.length = 0;
                                        changeDetected++;
                                    }
                                    newLength = newValue.length;
                                    if (oldLength !== newLength) {
                                        changeDetected++;
                                        oldValue.length = oldLength = newLength;
                                    }
                                    for (var i = 0; i < newLength; i++) {
                                        if (oldValue[i] !== newValue[i]) {
                                            changeDetected++;
                                            oldValue[i] = newValue[i];
                                        }
                                    }
                                } else {
                                    if (oldValue !== internalObject) {
                                        oldValue = internalObject = {};
                                        oldLength = 0;
                                        changeDetected++;
                                    }
                                    newLength = 0;
                                    for (key in newValue) {
                                        if (newValue.hasOwnProperty(key)) {
                                            newLength++;
                                            if (oldValue.hasOwnProperty(key)) {
                                                if (oldValue[key] !== newValue[key]) {
                                                    changeDetected++;
                                                    oldValue[key] = newValue[key];
                                                }
                                            } else {
                                                oldLength++;
                                                oldValue[key] = newValue[key];
                                                changeDetected++;
                                            }
                                        }
                                    }
                                    if (oldLength > newLength) {
                                        changeDetected++;
                                        for(key in oldValue) {
                                            if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) {
                                                oldLength--;
                                                delete oldValue[key];
                                            }
                                        }
                                    }
                                }
                                return changeDetected;
                            }
                            function $watchCollectionAction() {
                                listener(newValue, oldValue, self);
                            }
                            return this.$watch($watchCollectionWatch, $watchCollectionAction);
                        },
                        $digest: function() {
                            ........
                        },
                        $destroy: function() {
                            ......
                        },
                        $eval: function(expr, locals) {
                            .....
                        },
                        $evalAsync: function(expr) {
                            .......
                        },
                        $$postDigest : function(fn) {
                            ........
                        },
                        $apply: function(expr) {
                            ......
                        },
                        $on: function(name, listener) {
                            ......
                        },
                        $emit: function(name, args) {
                            .......
                        },
                        $broadcast: function(name, args) {
                          ......
                        }
                    };
                    var $rootScope = new Scope();
                    return $rootScope;
                    function beginPhase(phase) {
                        if ($rootScope.$$phase) {
                            throw $rootScopeMinErr('inprog', '{0} already in progress', $rootScope.$$phase);
                        }
                        $rootScope.$$phase = phase;
                    }
                    function clearPhase() {
                        $rootScope.$$phase = null;
                    }
                    function compileToFn(exp, name) {
                        var fn = $parse(exp);
                        assertArgFn(fn, name);
                        return fn;
                    }
                    function decrementListenerCount(current, count, name) {
                        do {
                            current.$$listenerCount[name] -= count;
                            if (current.$$listenerCount[name] === 0) {
                                delete current.$$listenerCount[name];
                            }
                        } while ((current = current.$parent));
                    }
                    function initWatchVal() {}

    从代码中,可以看到。第一步,定义了一个构造函数function Scope(){},第二步,定义了Scope的原型Scope.prototype = {},第三步,new出来了一个Scope,赋值给了$rootScope,并return这个Scope实例对象。

    通过第三步大家可以看出,angularJS默认会创建根作用域$rootScope,并作为$rootScopeprovider的实例对象返回。

    第一步,初始化了Scope对象的一些属性值。

    第二步,定义了一些方法。我们来详细讲解这些方法的作用:

    $new: function(isolate) {
                            var ChildScope,
                                child;
    
                            if (isolate) {
                                child = new Scope();
                                child.$root = this.$root;
                                // ensure that there is just one async queue per $rootScope and its children
                                child.$$asyncQueue = this.$$asyncQueue;
                                child.$$postDigestQueue = this.$$postDigestQueue;
                            } else {
                                ChildScope = function() {}; // should be anonymous; This is so that when the minifier munges
                                // the name it does not become random set of chars. This will then show up as class
                                // name in the web inspector.
                                ChildScope.prototype = this;
                                child = new ChildScope();
                                child.$id = nextUid();
                            }
                            child['this'] = child;
                            child.$$listeners = {};
                            child.$$listenerCount = {};
                            child.$parent = this;
                            child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null;
                            child.$$prevSibling = this.$$childTail;
                            if (this.$$childHead) {
                                this.$$childTail.$$nextSibling = child;
                                this.$$childTail = child;
                            } else {
                                this.$$childHead = this.$$childTail = child;
                            }
                            return child;
     },

    此方法的作用是用来创建子作用域。

    • isolate变量的值决定是执行if语句的代码还是else语句的代码,如果isolate的值是true,那么就会创建一个独立的作用域。这个在我们创建指令,并且在创建指令的回调方法中有scope属性的情况下,会出现这种情况,当然还有其他别的特殊情况下也会这样。假如是独立作用域的话,会多一个$root属性,它的值默认指向rootscope。

    • 如果isolate的值为false,则会定义一个空的构造函数ChildScope ,并且把此构造函数的prototype指向当前scope的实例对象,然后,new一个ChildScope 对象。

    • 然后为生成的子作用域设置$parent属性为当前作用域,并且设置子作用域的一些默认的属性值,然后设置当前作用域的一些属性值为生成的子作用域。最后,返回这个新生成的子作用域child。

    接下来,我们再来说说$watch方法:

    $watch: function(watchExp, listener, objectEquality) {
                            var scope = this,
                                get = compileToFn(watchExp, 'watch'),
                                array = scope.$$watchers,
                                watcher = {
                                    fn: listener,
                                    last: initWatchVal,
                                    get: get,
                                    exp: watchExp,
                                    eq: !!objectEquality
                                };
                            lastDirtyWatch = null;
                            if (!isFunction(listener)) {
                                var listenFn = compileToFn(listener || noop, 'listener');
                                watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);};
                            }
                            if (typeof watchExp == 'string' && get.constant) {
                                var originalFn = watcher.fn;
                                watcher.fn = function(newVal, oldVal, scope) {
                                    originalFn.call(this, newVal, oldVal, scope);
                                    arrayRemove(array, watcher);
                                };
                            }
                            if (!array) {
                                array = scope.$$watchers = [];
                            }
                            array.unshift(watcher);
                            return function() {
                                arrayRemove(array, watcher);
                                lastDirtyWatch = null;
                            };
    }

    $watch方法有三个参数,第一个是监听表达式,可以是字符串也可以是方法,第二个是监听方法,第三个参数代表是否深度监听。

    方法里面首先初始化一个get局部变量,初始化get的compileToFn函数其实是调用$parse实例对象来解析监听参数的,它会返回一个方法赋给get属性,这个方法会在dirty check里用到,用来获取监听表达式的值。$parse实例对象是$parseprovider实例化生成的。

    接下来,初始化了一个watcher对象,此对象用来保存一些监听相关的信息:

    • fn: 代表监听方法,当监听表达式有变化时,也就说值改变时会执行此方法
    • last: 保存最后一次发生变化的监听表达式的值
    • get: 保存一个监听表达式对应的方法,目的是用来获取表达式的值然后用来进行新旧对比,看是否有变化
    • exp: 保存一个原始的监听表达式
    • eq: 保存$watch方法的第三个参数,表示是否进行深度比较

    然后会检查传递进来的监听方法是否为方法,如果不是,则通过parse方法解析生成一个方法listenFn ,然后通过包装这个listenFn方法生成一个方法watcher.fn

    ,此方法就是被称为监听方法,并且此方法体的内容就是执行刚才生成的方法listenFn,并默认传递当前作用域作为参数。

    接着会检查监听表达式是否为字符串,如果是并且监听表达式的constant为true,就进入if语句,这代表监听表达式这个字符串是一个常量。那么,angular在处理这种监听的时候,执行完一次监听方法之后就会删除这个$watch。

    最后,往当前作用域里的$$watchers数组中添加$watch对象,并返回一个方法。注意这里的返回值,利用JS的闭包保留了当前的watcher变量,这个返回值方法是用来删除监听用的。

    接下来,我们来说说$digest方法

    digest方法是dirty check的核心,它里面的代码是先执行$$asyncQueue队列中保存的表达式,然后开启一个traverseScopesLoop循环,来循环遍历$$watchers,如果watch与上一次的值不相同,也就是被改变了,就执行watch里的监听方法。假如ttl超过了angular默认设置的值,则dirth check结束。最后执行$$postDigestQueue队列里的表达式。

    $digest: function() {
                            var watch, value, last,
                                watchers,
                                asyncQueue = this.$$asyncQueue,
                                postDigestQueue = this.$$postDigestQueue,
                                length,
                                dirty, ttl = TTL,
                                next, current, target = this,
                                watchLog = [],
                                logIdx, logMsg, asyncTask;
                            beginPhase('$digest');
                            lastDirtyWatch = null;
                            do { 
                                dirty = false;
                                current = target;
                                while(asyncQueue.length) {
                                    try {
                                        asyncTask = asyncQueue.shift();
                                        asyncTask.scope.$eval(asyncTask.expression);
                                    } catch (e) {
                                        clearPhase();
                                        $exceptionHandler(e);
                                    }
                                    lastDirtyWatch = null;
                                }
                                traverseScopesLoop:
                                    do { 
                                        if ((watchers = current.$$watchers)) {
                                            length = watchers.length;
                                            while (length--) {
                                                try {
                                                    watch = watchers[length];
                                                    if (watch) {
                                                        if ((value = watch.get(current)) !== (last = watch.last) &&
                                                            !(watch.eq
                                                                ? equals(value, last)
                                                                : (typeof value == 'number' && typeof last == 'number'
                                                                && isNaN(value) && isNaN(last)))) {
                                                            dirty = true;
                                                            lastDirtyWatch = watch;
                                                            watch.last = watch.eq ? copy(value) : value;
                                                            watch.fn(value, ((last === initWatchVal) ? value : last), current);
                                                            if (ttl < 5) {
                                                                logIdx = 4 - ttl;
                                                                if (!watchLog[logIdx]) watchLog[logIdx] = [];
                                                                logMsg = (isFunction(watch.exp))
                                                                    ? 'fn: ' + (watch.exp.name || watch.exp.toString())
                                                                    : watch.exp;
                                                                logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
                                                                watchLog[logIdx].push(logMsg);
                                                            }
                                                        } else if (watch === lastDirtyWatch) {
                                                            dirty = false;
                                                            break traverseScopesLoop;
                                                        }
                                                    }
                                                } catch (e) {
                                                    clearPhase();
                                                    $exceptionHandler(e);
                                                }
                                            }
                                        }
                                        if (!(next = (current.$$childHead ||
                                            (current !== target && current.$$nextSibling)))) {
                                            while(current !== target && !(next = current.$$nextSibling)) {
                                                current = current.$parent;
                                            }
                                        }
                                    } while ((current = next));
                                if((dirty || asyncQueue.length) && !(ttl--)) {
                                    clearPhase();
                                    throw $rootScopeMinErr('infdig',
                                        '{0} $digest() iterations reached. Aborting!
    ' +
                                            'Watchers fired in the last 5 iterations: {1}',
                                        TTL, toJson(watchLog));
                                }
                            } while (dirty || asyncQueue.length);
                            clearPhase();
                            while(postDigestQueue.length) {
                                try {
                                    postDigestQueue.shift()();
                                } catch (e) {
                                    $exceptionHandler(e);
                                }
                            }
    },

    通过上面的代码,可以看出,里面的核心就是两个循环,外循环保证所有的model都能检测到(do{.....} while (dirty || asyncQueue.length);),内循环则是真正的检测每个watch(traverseScopesLoop: do{......}while((current = next));)。

    watch.get方法的作用是计算监听表达式的值,然后用当前的值去跟旧值进行对比,假如不相等,也就是改变了,就执行监听方法。

    注意这里的watch.eq,它的作用是是否进行深度检查,equals方法是angular里的方法,用来深度对比两个对象。这里的不相等有一个例外,那就是NaN === NaN,这个永远都是false,所以这里加了判断。

    比较完之后,把新值传给watch.last,然后执行watch.fn,也就是监听方法,传递三个参数给它,分别是:最新计算的值,上次计算的值(如果是第一次,则传递新值),最后一个参数是当前作用域的实例对象。

    内循环里面有一个设置外循环的条件值,那就是dirty = true,也就是说只要内循环执行了一次watch,则外循环还要继续执行,这是为了保证所有的model都能监测一次,虽然这个有点浪费性能,不过超过ttl设置的值,dirty check就会强制关闭,并抛出异常。

    这里的watchLog日志对象是在内循环里,当ttl低于5的时候开始记录的。

    当检查完一个作用域内的所有watch之后,则开始深度遍历当前作用域的子级作用域或者父级作用域,虽然这有些影响性能。它就是不断的查找当前作用域的子级,没有子级,则开始查找兄弟节点,最后查找它的父级,是一个深度遍历查找。只要next有值,则内循环一直执行。

    不过内循环也有跳出的情况,那就是当前的watch跟最后一次检查的watch相等时,就退出内循环。

    注意:这个内循环是一个label(标签)语句,这个可以在循环中执行跳出操作,就像上面的break traverseScopesLoop。

    正常执行完两个循环之后,清除当前的阶段标识clearPhase();,然后开始执行postDigestQueue队列里的表达式。

    接下来我们来说下$destroy方法

    $destroy: function() {
                            if (this.$$destroyed) return;
                            var parent = this.$parent;
                            this.$broadcast('$destroy');
                            this.$$destroyed = true;
                            if (this === $rootScope) return;
                            forEach(this.$$listenerCount, bind(null, decrementListenerCount, this));
                            if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
                            if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
                            if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
                            if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
                            this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead =
                                this.$$childTail = null;
    },

    这个方法是用来销毁当前作用域的,原理主要是清空当前作用域内的一些属性,以免执行digest,$emit,$broadcast时会关联到。

    里面的代码比较简单,先是通过foreach语句来遍历$$listenerCount属性,清空$$listeners,$$watchers,$$asyncQueue,$$postDigestQueue里面的值,然后再设置$parent,$$nextSibling,$$prevSibling,$$childHead,$$childTail为null。

    接下来我们来说下$eval方法

    此方法可以直接在angular程序里执行一个字符串表达式:

     $eval: function(expr, locals) {
          return $parse(expr)(this, locals);
     },

    它的源码其实就是通过parse方法把你传进去的字符串表达式解析成一个执行表达式的方法,然后传递当前作用域以及额外的参数执行此方法。

    接下来我们来说下$evalAsync方法

    evalAsync方法的作用就是延迟执行表达式,并且执行完成后,不管是否异常,都触发dirty check。

    $evalAsync: function(expr) {
                            if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
                                $browser.defer(function() {
                                    if ($rootScope.$$asyncQueue.length) {
                                        $rootScope.$digest();
                                    }
                                });
                            }
                            this.$$asyncQueue.push({scope: this, expression: expr});
    },

    从上面的源码中可以看到,当前作用域内部有一个$$asyncQueue异步队列,它保存着所有需要延迟执行的表达式,此处的表达式可以是字符串也可以是方法,因为这个表达式最终会调用$eval方法。注意这里调用了$browser服务的defer方法,此方法是调用setTimeout来实现的,它的作用是延迟执行方法。

    接下来我们来说下$postDigest方法

    $$postDigest : function(fn) {
              this.$$postDigestQueue.push(fn);
    },

    这个方法跟evalAsync不同的是,它不会主动触发digest方法,只是往postDigestQueue队列中添加执行表达式。它会在digest方法体内最后被执行,相当于在触发dirty check之后,我们可以执行别的一些逻辑。

    接下来,我们来说下$apply方法

    $apply: function(expr) {
                            try {
                                beginPhase('$apply');
                                return this.$eval(expr);
                            } catch (e) {
                                $exceptionHandler(e);
                            } finally {
                                clearPhase();
                                try {
                                    $rootScope.$digest();
                                } catch (e) {
                                    $exceptionHandler(e);
                                    throw e;
                                }
                            }
    },

    如果不在angularjs的上下文中执行js,但是想操作angular中的值,则必须使用此方法了。比如:在原生的DOM事件中执行想改变angular中某些model的值,这个时候就要使用$apply方法了。

    代码中,首先让当前阶段标识变为$apply,然后执行$eval方法, 这个方法上面有讲到,最后执行$digest方法,使angular中的model改变。

    接下来,我们来说说scope中的event模块,它提供$on,$emit,$broadcast三个方法:

    $on方法

    $on: function(name, listener) {
                            var namedListeners = this.$$listeners[name];
                            if (!namedListeners) {
                                this.$$listeners[name] = namedListeners = [];
                            }
                            namedListeners.push(listener);
                            var current = this;
                            do {
                                if (!current.$$listenerCount[name]) {
                                    current.$$listenerCount[name] = 0;
                                }
                                current.$$listenerCount[name]++;
                            } while ((current = current.$parent));
                            var self = this;
                            return function() {
                                namedListeners[indexOf(namedListeners, listener)] = null;
                                decrementListenerCount(self, 1, name);
                            };
    },

    这个方法是用来绑定事件的,里面用到了两个实例变量$$listeners, $$listenerCount,分别用来保存事件,以及事件数量计数。

    分析上面的代码,可以看出每当绑定一个事件的时候,都会向$$listeners对象中添加name的属性,属性值就是事件回调函数。注意这里有个事件计数器,只要有父级,则也给父级的$$listenerCount添加name的属性,并且把name的值+1。最后这个方法返回一个取消事件的方法,在方法中会先设置$$listeners中name的属性值为null,然后调用decrementListenerCount使该事件的计数器-1。

    $emit方法

    $emit: function(name, args) {
                            var empty = [],
                                namedListeners,
                                scope = this,
                                stopPropagation = false,
                                event = {
                                    name: name,
                                    targetScope: scope,
                                    stopPropagation: function() {stopPropagation = true;},
                                    preventDefault: function() {
                                        event.defaultPrevented = true;
                                    },
                                    defaultPrevented: false
                                },
                                listenerArgs = concat([event], arguments, 1),
                                i, length;
                            do {
                                namedListeners = scope.$$listeners[name] || empty;
                                event.currentScope = scope;
                                for (i=0, length=namedListeners.length; i<length; i++) {
                                    if (!namedListeners[i]) {
                                        namedListeners.splice(i, 1);
                                        i--;
                                        length--;
                                        continue;
                                    }
                                    try {
                                        namedListeners[i].apply(null, listenerArgs);
                                    } catch (e) {
                                        $exceptionHandler(e);
                                    }
                                }
                                if (stopPropagation) return event;
                                scope = scope.$parent;
                            } while (scope);
                            return event;
    },

    这个方法是用来触发$on绑定的事件的。原理就是循环$$listeners,检查它里面是否有值,有的话,则执行。然后依次往上检查父级,这个方法有点类似浏览器的事件冒泡机制。

    上面的代码比较简单,首先定义一个事件对象的变量event,然后开启一个循环,只要scope中有值,则一直执行。这个方法的事件链是向上传递的,不过如果在事件回调函数中执行stopPropagation方法,则会停止向上传递事件。

    $broadcast方法

    $broadcast: function(name, args) {
                            var target = this,
                                current = target,
                                next = target,
                                event = {
                                    name: name,
                                    targetScope: target,
                                    preventDefault: function() {
                                        event.defaultPrevented = true;
                                    },
                                    defaultPrevented: false
                                },
                                listenerArgs = concat([event], arguments, 1),
                                listeners, i, length;
                            while ((current = next)) {
                                event.currentScope = current;
                                listeners = current.$$listeners[name] || [];
                                for (i=0, length = listeners.length; i<length; i++) {
                                    if (!listeners[i]) {
                                        listeners.splice(i, 1);
                                        i--;
                                        length--;
                                        continue;
                                    }
                                    try {
                                        listeners[i].apply(null, listenerArgs);
                                    } catch(e) {
                                        $exceptionHandler(e);
                                    }
                                }
                                if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||
                                    (current !== target && current.$$nextSibling)))) {
                                    while(current !== target && !(next = current.$$nextSibling)) {
                                        current = current.$parent;
                                    }
                                }
                            }
                            return event;
    }

    这个是$emit的升级版,广播事件。它既能向上传递事件,也能向下传递事件,还能平级传递事件。核心原理就是利用深度遍历当前作用域。

    代码跟$emit差不多,不同的是,这个方法是不断的取next值,而next的值则是通过深度遍历它的子级节点,兄弟节点,父级节点,并依次查找可用的名为name的事件回调函数。$broadcast方法不能常用,因为性能不是很理想。

    angular是通过观察者模式,把一个model绑定到多个view中的。

    如何知道model发生了变化,其实就是通过上面代码中的$watch和$digest(我们称这两个操作为脏值检测)。

    如果model是深层次嵌套的结构,我们是通过对象的深比较,来知道model的某个属性是不是变化了。

    上面代码中的ttl是什么意思呢?假设a和b两个方法互相watch对方,这时,a变化了,b就会变化,b变化了,a就会变化,形成一个死循环,angular内部就是通过ttl(默认为10)的值来判定,如果一个对象的值发生10次检测的过程还在变化,那么angular会强制停止,并抛出一个异常。

    angular在双向绑定的时候,是如何支持表达式的呢?其实就是通过angular自带的编译器$parser对表达式进行解析做到的。

    加油!

  • 相关阅读:
    Python/WSGI 应用快速入门--转
    汇编题目:数字转字符,并在窗口上显示出来
    汇编题目:在窗口上显示Welcome to masm!
    VBA中的函数Timer用法
    用VBA计算两个日期之间的工作日(去掉周末两天)
    VBA记录当前系统时间并精确到毫秒
    上海房产税免征--积分或居住证
    学习汇编语言
    “Hello World”—— 第一个汇编程序
    汇编程序设计上机步骤
  • 原文地址:https://www.cnblogs.com/chaojidan/p/4272172.html
Copyright © 2011-2022 走看看