首頁 >web前端 >js教程 >[譯] Angular DOM 更新機制 - Laravel/Angular 技術分享

[譯] Angular DOM 更新機制 - Laravel/Angular 技術分享

寻∝梦
寻∝梦原創
2018-09-07 16:24:051339瀏覽
這篇文章主要的向大家介紹了angularjs dom的更新機制,還有關於angularjs的模型表達式和程式內部架構等很多知識點,下面就讓我們一起開始學習吧

angularjs的模型表達式:

由模型變更觸發的DOM 更新是所有前端框架的重要功能(註:即保持model 和view 的同步),當然Angular 也不例外。定義一個如下模板表達式:

<span>Hello {{name}}</span>

或類似下面的屬性綁定(註:這與上面代碼等價):

<span [textContent]="&#39;Hello &#39; + name"></span>

當每次name 值改變時,Angular 會神奇般的自動更新DOM 元素(註:最上面程式碼是更新DOM 文字節點,上面程式碼是更新DOM 元素節點,兩者是不一樣的,下文解釋)。這表面上看起來很簡單,但其內部工作卻相當複雜。而且,DOM 更新只是Angular 變更偵測機制 的一部分,變更偵測機制主要由以下三個步驟組成:

  • DOM updates(註:即本文將要解釋的內容)

  • child components Input bindings updates

  • ##query list updates

本文主要探討變更偵測機制的渲染部分(即DOM updates 部分)。如果你之前也對這個問題很好奇,可以繼續讀下去,絕對讓你茅塞頓開。

在引用相關原始碼時,假設程式是以生產模式運作。讓我們開始吧!

程式內部架構

在探索 DOM 更新之前,我們先搞清楚 Angular 程式內部究竟是如何設計的,簡單回顧一下吧。

視圖

從我的這篇文章

Here is what you need to know about dynamic components in Angular 知道Angular 編譯器會把程式中使用的元件編譯為一個工廠類別(factory)。例如,下面程式碼展示Angular 如何從工廠類中創建一個元件(註:這裡作者邏輯貌似有點亂,前一句說的Angular 編譯器編譯的工廠類,其實是編譯器去做的,不需要開發者做任何事情,是自動化的事情;而下面程式碼說的是開發者如何手動透過ComponentFactory 來建立一個Component 實例。總之,他是想說元件是怎麼被實例化的):

const factory = r.resolveComponentFactory(AComponent);
componentRef: ComponentRef<AComponent> = factory.create(injector);
Angular 使用這個工廠類別來實例化

View Definition ,然後使用viewDef 函數來建立視圖。 Angular 內部把一個程式看作為一個視圖樹,一個程式雖然有眾多元件,但有一個公共的視圖定義介面來定義由元件產生的視圖結構(註:即ViewDefinition Interface),當然Angular 使用每一個元件物件來建立對應的視圖,從而由多個視圖組成視圖樹。 (註:這裡有一個主要概念就是檢視,其架構為 ViewDefinition Interface

元件工廠

元件工廠大部分程式碼是由編譯器產生的不同視圖節點組成的,這些視圖節點是透過模板解析產生的(註:編譯器產生的元件工廠是一個傳回值為函數的函數,上文的ComponentFactory 是Angular 提供的類,供手動調用。當然,兩者指向同一個事物,只是表現形式不同而已)。假設定義一個元件的模板如下:

<span>I am {{name}}</span>
編譯器會解析這個模板產生包含如下類似的元件工廠程式碼(註:這只是最重要的部分程式碼):

function View_AComponent_0(l) {
    return jit_viewDef1(0,
        [
          jit_elementDef2(0,null,null,1,'span',...),
          jit_textDef3(null,['I am ',...])
        ], 
        null,
        function(_ck,_v) {
            var _co = _v.component;
            var currVal_0 = _co.name;
            _ck(_v,1,0,currVal_0);
註:由AppComponent 元件編譯產生的工廠函數完整程式碼如下
 (function(jit_createRendererType2_0,jit_viewDef_1,jit_elementDef_2,jit_textDef_3) {
     var styles_AppComponent = [''];
     var RenderType_AppComponent = jit_createRendererType2_0({encapsulation:0,styles:styles_AppComponent,data:{}});
     function View_AppComponent_0(_l) {
         return jit_viewDef_1(0,
            [
                (_l()(),jit_elementDef_2(0,0,null,null,1,'span',[],null,null,null,null,null)),
                (_l()(),jit_textDef_3(1,null,['I am ','']))
            ],
            null,
            function(_ck,_v) {
                var _co = _v.component;
                var currVal_0 = _co.name;
                _ck(_v,1,0,currVal_0);
           });
    }
 return {RenderType_AppComponent:RenderType_AppComponent,View_AppComponent_0:View_AppComponent_0};})
上面程式碼描述了視圖的結構,並在實例化元件時會被呼叫。

jit_viewDef_1 其實就是viewDef 函數,用來建立視圖(註:viewDef 函數很重要,因為視圖是呼叫它所建立的,產生的視圖結構就是ViewDefinition)。

viewDef 函數的第二個參數 nodes 有些類似 html 中節點的意思,但卻不只如此。上面程式碼中第二個參數是一個數組,其第一個數組元素 jit_elementDef_2 是元素節點定義,第二個數組元素 jit_textDef_3 是文字節點定義。 Angular 編譯器會產生許多不同的節點定義,節點類型是由 NodeFlags 設定的。稍後我們將看到 Angular 如何根據不同節點類型來做 DOM 更新。

本文只對元素和文字節點感興趣:

export const enum NodeFlags {
    TypeElement = 1 << 0, 
    TypeText = 1 << 1
讓我們簡單擼一遍。

注:上文作者说了一大段,其实核心就是,程序是一堆视图组成的,而每一个视图又是由不同类型节点组成的。而本文只关心元素节点和文本节点,至于还有个重要的指令节点在另一篇文章。

元素节点的结构定义

元素节点结构 是 Angular 编译每一个 html 元素生成的节点结构,它也是用来生成组件的,如对这点感兴趣可查看 Here is why you will not find components inside Angular。元素节点也可以包含其他元素节点和文本节点作为子节点,子节点数量是由 childCount 设置的。

所有元素定义是由 elementRef 函数生成的,而工厂函数中的 jit_elementDef_2() 就是这个函数。elementRef() 主要有以下几个一般性参数:

Name Description
childCount specifies how many children the current element have
namespaceAndName the name of the html element(注:如 'span')
fixedAttrs attributes defined on the element

还有其他的几个具有特定性能的参数:

Name Description
matchedQueriesDsl used when querying child nodes
ngContentIndex used for node projection
bindings used for dom and bound properties update
outputs, handleEvent used for event propagation

本文主要对 bindings 感兴趣。

注:从上文知道视图(view)是由不同类型节点(nodes)组成的,而元素节点(element nodes)是由 elementRef 函数生成的,元素节点的结构是由 ElementDef 定义的。

文本节点的结构定义

文本节点结构 是 Angular 编译每一个 html 文本 生成的节点结构。通常它是元素定义节点的子节点,就像我们本文的示例那样(注:<span>I am {{name}}</span>span 是元素节点,I am {{name}} 是文本节点,也是 span 的子节点)。这个文本节点是由 textDef 函数生成的。它的第二个参数以字符串数组形式传进来(注: Angular v5.* 是第三个参数)。例如,下面的文本:

<h1>Hello {{name}} and another {{prop}}</h1>

将要被解析为一个数组:

["Hello ", " and another ", ""]

然后被用来生成正确的绑定:

{
  text: 'Hello',
  bindings: [
    {
      name: 'name',
      suffix: ' and another '
    },
    {
      name: 'prop',
      suffix: ''
    }
  ]
}

在脏检查(注:即变更检测)阶段会这么用来生成文本:

text
+ context[bindings[0][property]] + context[bindings[0][suffix]]
+ context[bindings[1][property]] + context[bindings[1][suffix]]
注:同上,文本节点是由 textDef 函数生成的,结构是由 TextDef 定义的。既然已经知道了两个节点的定义和生成,那节点上的属性绑定, Angular 是怎么处理的呢?

节点的绑定

Angular 使用 BindingDef 来定义每一个节点的绑定依赖,而这些绑定依赖通常是组件类的属性。在变更检测时 Angular 会根据这些绑定来决定如何更新节点和提供上下文信息。具体哪一种操作是由 BindingFlags 决定的,下面列表展示了具体的 DOM 操作类型:

Name Construction in template
TypeElementAttribute attr.name
TypeElementClass class.name
TypeElementStyle style.name

元素和文本定义根据这些编译器可识别的绑定标志位,内部创建这些绑定依赖。每一种节点类型都有着不同的绑定生成逻辑(注:意思是 Angular 会根据 BindingFlags 来生成对应的 BindingDef)。(想看更多就到PHP中文网AngularJS开发手册中学习)

更新渲染器

最让我们感兴趣的是 jit_viewDef_1 中最后那个参数:

function(_ck,_v) {
   var _co = _v.component;
   var currVal_0 = _co.name;
   _ck(_v,1,0,currVal_0);
});

这个函数叫做 updateRenderer。它接收两个参数:_ck_v_ckcheck 的简写,其实就是 prodCheckAndUpdateNode 函数,而 _v 就是当前视图对象。updateRenderer 函数会在 每一次变更检测时 被调用,其参数 _ck_v 也是这时被传入。

updateRenderer 函数逻辑主要是,从组件对象的绑定属性获取当前值,并调用 _ck 函数,同时传入视图对象、视图节点索引和绑定属性当前值。重要一点是 Angular 会为每一个视图执行 DOM 更新操作,所以必须传入视图节点索引参数(注:这个很好理解,上文说了 Angular 会依次对每一个 view 做模型视图同步过程)。你可以清晰看到 _ck 参数列表:

function prodCheckAndUpdateNode(
    view: ViewData, 
    nodeIndex: number, 
    argStyle: ArgumentType, 
    v0?: any, 
    v1?: any, 
    v2?: any,

nodeIndex 是视图节点的索引,如果你模板中有多个表达式:

<h1>Hello {{name}}</h1>
<h1>Hello {{age}}</h1>

编译器生成的 updateRenderer 函数如下:

var _co = _v.component;

// here node index is 1 and property is `name`
var currVal_0 = _co.name;
_ck(_v,1,0,currVal_0);

// here node index is 4 and bound property is `age`
var currVal_1 = _co.age;
_ck(_v,4,0,currVal_1);

更新 DOM

现在我们已经知道 Angular 编译器生成的所有对象(注:已经有了 view,element node,text node 和 updateRenderer 这几个道具),现在我们可以探索如何使用这些对象来更新 DOM。

从上文我们知道变更检测期间 updateRenderer 函数传入的一个参数是 _ck 函数,而这个函数就是 prodCheckAndUpdateNode。这个函数在继续执行后,最终会调用 checkAndUpdateNodeInline ,如果绑定属性的数量超过 10,Angular 还提供了 checkAndUpdateNodeDynamic 这个函数(注:两个函数本质一样)。

checkAndUpdateNodeInline 函数会根据不同视图节点类型来执行对应的检查更新函数:

case NodeFlags.TypeElement   -> checkAndUpdateElementInline
case NodeFlags.TypeText      -> checkAndUpdateTextInline
case NodeFlags.TypeDirective -> checkAndUpdateDirectiveInline

让我们看下这些函数是做什么的,至于 NodeFlags.TypeDirective 可以查看我写的文章 The mechanics of property bindings update in Angular

注:因为本文只关注 element node 和 text node

元素节点

对于元素节点,会调用函数 checkAndUpdateElementInline 以及 checkAndUpdateElementValuecheckAndUpdateElementValue 函数会检查绑定形式是否是 [attr.name, class.name, style.some] 或是属性绑定形式:

case BindingFlags.TypeElementAttribute -> setElementAttribute
case BindingFlags.TypeElementClass     -> setElementClass
case BindingFlags.TypeElementStyle     -> setElementStyle
case BindingFlags.TypeProperty         -> setElementProperty;

然后使用渲染器对应的方法来对该节点执行对应操作,比如使用 setElementClass 给当前节点 span 添加一个 class

文本节点

对于文本节点类型,会调用 checkAndUpdateTextInline ,下面是主要部分:

if (checkAndUpdateBinding(view, nodeDef, bindingIndex, newValue)) {
    value = text + _addInterpolationPart(...);
    view.renderer.setValue(DOMNode, value);
}

它会拿到 updateRenderer 函数传过来的当前值(注:即上文的 _ck(_v,4,0,currVal_1);),与上一次变更检测时的值相比较。视图数据包含有 oldValues 属性,如果属性值如 name 发生变化,Angular 会使用最新 name 值合成最新的字符串文本,如 Hello New World,然后使用渲染器更新 DOM 上对应的文本。(想看更多就到PHP中文网AngularJS开发手册中学习)

注:更新元素节点和文本节点都提到了渲染器(renderer),这也是一个重要的概念。每一个视图对象都有一个 renderer 属性,即是 Renderer2 的引用,也就是组件渲染器,DOM 的实际更新操作由它完成。因为 Angular 是跨平台的,这个 Renderer2 是个接口,这样根据不同 Platform 就选择不同的 Renderer。比如,在浏览器里这个 Renderer 就是 DOMRenderer,在服务端就是 ServerRenderer,等等。从这里可看出,Angular 框架设计做了很好的抽象。

结论

我知道有大量难懂的信息需要消化,但是只要理解了这些知识,你就可以更好的设计程序或者去调试 DOM 更新相关的问题。我建议你按照本文提到的源码逻辑,使用调试器或 debugger 语句 一步步去调试源码。

好了,這篇文章到這就結束了(想看更多就到PHP中文網AngularJS使用手冊中學習),有問題的可以在下方留言提問。

以上是[譯] Angular DOM 更新機制 - Laravel/Angular 技術分享的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn