文章参考html
我的博客: www.morphzhou.cn数组
所谓双向数据绑定,概念上为数据模型到视图的绑定,以及视图到数据模型的绑定。容易理解的说法就是从界面的操做能实时反映到数据,数据的变动能实时展示到界面。好比Angular中的一个双向数据绑定的示例:app
{{yourname}}经过ng-model与input的value绑定,当input的value改变的时候<h1>内的值就会相应改变async
双向数据绑定的优势是无需进行和单向数据绑定的那些CRUD(Create,Retrieve,Update,Delete)操做函数
目前对于双向数据绑定的实现有这么几种流派oop
脏值检测,例如AngularJS源码分析
Getter/Setter,例如Vue.jspost
对于Getter/Setter实现的数据双向绑定来讲,核心在于重定义model的getter与setter方法,在数据变更的时候从新渲染页面。两种方式各有优劣。
当咱们使用Getter/Setter的时候,每次修改数值都会激活刷新模版的方法,而脏值检测则能够在完成全部数值变更后,统一刷新到Dom。可是当监听元素变多的时候,watcher列表会变得很长,查询变更的数据元素将耗费更多的资源。
源码版本 Angular-1.5.0 angular.js
在Angular当中,有个贯穿始终的对象$scope。Scope本质为一个构造函数,而$scope就是Scope的实例。源码16028行
function Scope() { this.$id = nextUid(); this.$$phase = this.$parent = this.$$watchers = this.$$nextSibling = this.$$prevSibling = this.$$childHead = this.$$childTail = null; this.$root = this; this.$$destroyed = false; this.$$listeners = {}; this.$$listenerCount = {}; this.$$watchersCount = 0; this.$$isolateBindings = null; }
在Scope的原型(Scope.prototype)中共定义了13个函数。其中有两个函数对双向数据绑定起着相当重要的做用:监视对象属性。
$watch
$digest
$watch和$digest是同一个硬币的两面。它们两者同时造成了$digest循环的核心:对数据的变化作出反应。可使用$watch函数为scope添加一个监视器。当这个scope中有变化发生时,监视器便会提醒你。
$watch 源码16247行
$watch: function(watchExp, listener, objectEquality, prettyPrintExpression) { var get = $parse(watchExp); if (get.$$watchDelegate) { return get.$$watchDelegate(this, listener, objectEquality, get, watchExp); } var scope = this, array = scope.$$watchers, watcher = { fn: listener, last: initWatchVal, get: get, exp: prettyPrintExpression || watchExp, eq: !!objectEquality }; lastDirtyWatch = null; if (!isFunction(listener)) { watcher.fn = noop; } if (!array) { array = scope.$$watchers = []; } // we use unshift since we use a while loop in $digest for speed. // the while loop reads in reverse order. array.unshift(watcher); incrementWatchersCount(this, 1); return function deregisterWatch() { if (arrayRemove(array, watcher) >= 0) { incrementWatchersCount(scope, -1); } lastDirtyWatch = null; }; }
为了监视一个变量的变化,可使用$scope.$watch函数。这个函数的前两个,它指明了要观察什么(watchExp),在变化时要发生什么(listener)。
$scope.name = 'Morph_Zhou'; $scope.$watch( function( ) { return $scope.name; }, function( newValue, oldValue ) { console.log('$scope.name was updated!'); } );
在Scope中有一个对象数组$$watchers,里面保存着咱们定义的全部的监视器对象watcher。$watch函数将会返回一个deregisterWatch函数。这意味着若是咱们使用$scope.$watch对一个变量进行监视,咱们也能够在之后经过调用某个函数来中止监视。
另一个是$digest函数。它迭代了全部绑定到scope中的监视器,而后进行监视并运行相应的监听函数。
$digest 源码16607行
$digest: function() { var watch, value, last, fn, get, watchers, length, dirty, ttl = TTL, next, current, target = this, watchLog = [], logIdx, logMsg, asyncTask; beginPhase('$digest'); // Check for changes to browser url that happened in sync before the call to $digest $browser.$$checkUrlChange(); if (this === $rootScope && applyAsyncId !== null) { // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then // cancel the scheduled $apply and flush the queue of expressions to be evaluated. $browser.defer.cancel(applyAsyncId); flushApplyAsync(); } lastDirtyWatch = null; do { // "while dirty" loop dirty = false; current = target; while (asyncQueue.length) { try { asyncTask = asyncQueue.shift(); asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals); } catch (e) { $exceptionHandler(e); } lastDirtyWatch = null; } traverseScopesLoop: do { // "traverse the scopes" loop if ((watchers = current.$$watchers)) { // process our watches length = watchers.length; while (length--) { try { watch = watchers[length]; // Most common watches are on primitives, in which case we can short // circuit it with === operator, only when === fails do we use .equals if (watch) { get = watch.get; if ((value = 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, null) : value; fn = watch.fn; fn(value, ((last === initWatchVal) ? value : last), current); if (ttl < 5) { logIdx = 4 - ttl; if (!watchLog[logIdx]) watchLog[logIdx] = []; watchLog[logIdx].push({ msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp, newVal: value, oldVal: last }); } } else if (watch === lastDirtyWatch) { // If the most recently dirty watcher is now clean, short circuit since the remaining watchers // have already been tested. dirty = false; break traverseScopesLoop; } } } catch (e) { $exceptionHandler(e); } } } // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $broadcast if (!(next = ((current.$$watchersCount && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while (current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } } while ((current = next)); // `break traverseScopesLoop;` takes us to here if ((dirty || asyncQueue.length) && !(ttl--)) { clearPhase(); throw $rootScopeMinErr('infdig', '{0} $digest() iterations reached. Aborting!\n' + 'Watchers fired in the last 5 iterations: {1}', TTL, watchLog); } } while (dirty || asyncQueue.length); clearPhase(); while (postDigestQueue.length) { try { postDigestQueue.shift()(); } catch (e) { $exceptionHandler(e); } } }
$digest函数将会在$rootScope中被$scope.$apply所调用。它将会在$rootScope中运行digest循环,而后向下遍历每个做用域并在每一个做用域上运行循环。在简单的情形中,digest循环将会触发全部位于$$watchers变量中的全部watchExp函数,将它们和最新的值进行对比,若是值不相同,就会触发监听器。当digest循环运行时,它将会遍历全部的监听器而后再次循环,只要此次循环发现了”脏值”,循环就会继续下去。若是watchExp的值和最新的值不相同,那么此次循环就会被认为发现了“脏值”。
实际上双向数据绑定的功能远远不止这么一些,这里仅仅是极尽简化的版本。若是想实现一个功能较为齐全的,能够参考慕课网上大漠穷秋的一节课程当中的要求。
首先咱们先要模仿Angular设置本身的scope,咱们只须要简单的实现一下$watch,以及$digest方法。$watch函数须要接受两个参数,watchExp和listener。当$watch被调用时,咱们须要将它们push进入到Scope的$$watcher数组中。若是没有提供listener,咱们会将listener设置为一个空函数,这样一来咱们能够$watch全部的变量。接下来咱们将会建立$digest。咱们须要来检查旧值是否等于新的值,若是两者不相等,监听器就会被触发。咱们会一直循环这个过程,直到两者相等。
var Scope = function() { this.$$watchers = []; }; Scope.prototype.$watch = function(watchExp, listener) { this.$$watchers.push({ watchExp: watchExp, listener: listener || function() {} }); }; Scope.prototype.$digest = function() { var dirty; do { dirty = false; for (var i = 0; i < this.$$watchers.length; i++) { var newValue = this.$$watchers[i].watchExp(), oldValue = this.$$watchers[i].last; if (oldValue !== newValue) { this.$$watchers[i].listener(newValue, oldValue); dirty = true; this.$$watchers[i].last = newValue; } } } while (dirty); };
若是咱们把$digest函数绑定到一个input元素的keyup事件上。
var $scope = new Scope(); $scope.name = 'Morph_Zhou'; var element = document.querySelectorAll("input"); element[0].onkeyup = function() { $scope.name = element[0].value; $scope.$digest(); }; $scope.$watch(function() { return $scope.name; }, function(newValue, oldValue) { console.log('Input value updated - it is now ' + newValue); element[0].value = $scope.name; }); var updateScopeValue = function updateScopeValue() { $scope.name = 'Morph_Gaming'; $scope.$digest(); };
使用上面的代码,不管什么时候咱们改变了input的值,$scope中的name属性都会相应的发生变化。