angular 1.0源代碼分析
本文分析angular 1.0從初始化開始到編譯網頁更新頁面的源代碼過程以及一些重要細節。
測試項目例子:
<html ng-app=‘myapp‘ >
<body ng-controller="myController" >
<tip title="title"></tip>
</body>
controller和指令用angular.module的方法定義。
angular 1.0 以指令為中心,directive指令標簽就是組件,有template,而屬性指令主要是修改元素屬性,angular 2.0改為以組件為中心設計。
angular入口初始化程序:
function angularInit(element, bootstrap) {
bootstrap(appElement, module ? [module] : []);
}
function bootstrap(element, modules) {
var doBootstrap = function() {
modules = modules || [];
modules.unshift([‘$provide‘, function($provide) {
$provide.value(‘$rootElement‘, element);
}]);
modules.unshift(‘ng‘);
var injector = createInjector(modules);
injector.invoke([‘$rootScope‘, ‘$rootElement‘, ‘$compile‘, ‘$injector‘, ‘$animate‘, //angular代碼執行時已經創建內部對象,這些是對象的key(名字)
function(scope, element, compile, injector, animate) { // invoke([模塊1,模塊2,...,fn])就是調用執行fn,傳遞依賴模塊(angular內部對象)
scope.$apply(function() { // 外套$apply執行方法,執行完方法之後掃描watcher重新獲取所有watcher表達式的值進行必要的頁面更新
element.data(‘$injector‘, injector);
compile(element)(scope); // 編譯根元素,返回link函數,再執行link函數,更新頁面的代碼在每個指令表達式watcher的update方法中。
});
}]);
return injector;
};
return doBootstrap();
}
function createInternalInjector(cache, factory) {
function invoke(fn, self, locals){ //invoke就是變換參數和作用域調用函數, angular內部對象機制和依賴模塊註入非常復雜,本文忽略
fn = fn[length];
return fn.apply(self, args);
function compile($compileNodes, transcludeFn, maxPrior //從根元素開始編譯
var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, // 遞歸編譯子節點
function compileNodes(nodeList, transcludeFn, $rootElement,
applyDirectivesToNode(directives,
function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn,
$compileNode.html(directiveValue); // 把指令的template<div>{{title}}</div>插入網頁中的節點元素中
childLinkFn = compileNodes(childNodes,nodeLinkFn ? nodeLinkFn.transclude : transcludeFn); // 有子節點則遞歸調用自身
return linkFnFound ? compositeLinkFn : null;
//每一次遞歸子節點時已經編譯了子節點的指令,遞歸子節點層層返回到最上層compile代碼位置時,返回的link函數已經包含
每一層遞歸時產生的link函數,也就是每一層遞歸時編譯結果。最後只要再執行最終返回的link函數,傳遞根scope,把根scope保存
在根元素對象屬性中,就完成了整個編譯插入網頁的過程,在每一次遞歸編譯子節點時如果有指令會編譯指令。
如果是路由組件,有template,是編譯插入網頁生效。
function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) { //每一次遞歸返回的link函數,含每一次遞歸編譯的結果數據
return function publicLinkFn(scope, cloneConnectFn, // compile返回的link函數,返回後會執行link函數傳遞scope
$linkNode.eq(i).data(‘$scope‘, scope); // node是元素jquery對象,把scope保存到元素對象的屬性中
if (cloneConnectFn) cloneConnectFn($linkNode, scope);
if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode); // link函數含層層遞歸編譯結果數據
return $linkNode;
angular的link函數比較費解,它主要是關聯元素和scope/controller instance,一般情況下是沒用的,scope從rootscope開始就是一個樹結構,從rootscope可以遞歸所有的子scope,
controller的方法中引用某一個已經創建存在的scope,scope按id區分和索引,都和元素沒關系,數據層面與元素是沒有直接關系的。
編譯方法compile外套$apply,$apply會調用$digest:
$digest: function() {
//遞歸scope找watcher,watcher數據中沒有scope,因此執行watcher的方法時要傳遞scope,執行watcher的方法時會變換作用域為scope
value = watch.get(current) // 獲取表達式的值時要傳遞scope,表達式{{title}}的值是hello
watch.fn(value, ((last === initWatchVal) ? value : last), current); // 執行watch.fn之後網頁顯示hello,fn就是handler/update函數
代碼中還涉及到defer和settimeout是延遲函數,實現異步調度,非功能性流程。
因此angular在初始化時編譯網頁時針對每個表達式建立了watcher,編譯程序外套$apply,會調用$digest掃描執行watcher的update方法更新網頁。
vue也是在初始化編譯template時針對每個表達式建立watcher,為組件的data屬性建立set/get方法,只要set數據操作,就會執行相應的watcher的update更新頁面。
vue 2.0是針對組件建立watcher,初始化時編譯根組件執行根組件watcher的update更新頁面,set數據時執行相應的組件的watcher的update方法更新頁面,組件
可能有子組件嵌套,那麽從組件遞歸重新編譯template產生vnode,再根據vnode更新頁面。
angular是在數據操作之後執行$digest掃描執行watcher更新頁面,vue是set觸發執行watcher更新頁面。
react是在初始化編譯時遞歸編譯子節點,然後把編譯結果插入網頁生效,當數據變化時,通過Connect組件的Listener或setState執行組件的render()方法重新編譯組件
更新頁面。
angular和vue都是用compile方法遞歸編譯網頁元素,這是它們的核心程序,react也是類似的,其mountComponent其實就是遞歸編譯程序,因此所有的框架在基本原理方法方面其實都是用類似的設計方法,都是設計一個核心編譯程序,遞歸編譯所有的子節點。
在template和表達式的寫法方面不太一樣,react比較特殊,它用render()方法寫template,用JSX語法,用babel編譯解析,產生一個含層層嵌套的createElement()的函數,執行這個函數就產生一個根元素。
vue 2.0也是用類似的方法,編譯template產生一個render方法代碼,含層層嵌套的createElement方法,再執行render方法代碼產生一個根元素Element。
angular 1.0源代碼分析