Vue2.x原理剖析(二)の手書き1つの簡版Vue


前言
前回の記事Vue2.x応答式原理剖析(一)では、データ応答式の原理を明らかにしましたが、今日は前回の実装を利用して、簡単なVueを作成してみましょう.
MVVMクラスの作成
// TVue.js
/**
* @desc: TVue    MVVM  ,            Vue
*    @params {} options           
*/
class TVue {
  constructor (options) {
    this.$options = options
    this.$data = options.data
  }
}

TVueは作成時に2つのことをする必要があります.
  • は、入力されたデータに対して応答式処理を行う.
  • コンパイルテンプレート結果を
  • でレンダリング
    class TVue {
      constructor (options) {
        this.$options = options
        this.$data = options.data
             // 1.     
        observe(this.$data)
             // 2.  
        if (options.el) {
          this.$mount(options.el)
        }
      }
    }

    ⚠️私たちは普段data属性値を使用するときになぜ直接thisを通過できるのか.xxxアクセスは、thisを通過する必要はありません.data.xxxは訪問しますか?
    これは,Vueソースコードにエージェントを作り,vmインスタンスのdata属性値をvmインスタンスに直接エージェントしたためである.
    ここでは、プロキシメソッドを実装するためにソースコードを学ぶこともできます.
    function proxy (vm) {
      Object.keys(vm.$data).forEach(key=>{
        Object.defineProperty(vm, key, {
          get() {
            return vm.$data[key]
          },
          set(v) {
            vm.$data[key] = v
          }
        })
      })
    }
    //        TVue     :
    class TVue {
      constructor (options) {
        this.$options = options
        this.$data = options.data
             // 1.     
        observe(this.$data)
        // 1.5  $data   
        proxy(this)
             // 2.  
        if (options.el) {
          this.$mount(options.el)
        }
      }
    }

    observeメソッド実装
    前の文章の共有を通じて、Vue 2.xにはJS言語の特性Objectが利用する.defineProperty()では、オブジェクト属性getter/setterを定義して属性へのアクセスをブロックします.observeメソッドの作成と機能を振り返ってみましょう.
    function observe(obj) {
      if (typeof obj !== 'object' || typeof === null) {
             //                null     ,       
        return 
      }
      //      ,         Observer   
      new Observer(obj)
    }
    // Observer                   
    // defineReactive       getter/setter
    // getter      ,setter      
    class Observer {
      constructor(options) {
        if (Array.isArray(obj)) {
          // todo:        ,     Array.js                   
        } else {
          this.walk(options)
        }
      }
      walk(obj) {
             //                      
        Object.keys(obj).forEach(key => {
          defineReactive(obj, key, obj[key]
        })
      }
    }            
    function defineReactive (obj, key, val) {
        //       
      observe(val)
        Object.defineProperty(obj, key, {
        get () {
          // todo:     
          return val
        },
        set(newVal) {
          if (newVal !== val) {
            val = newVal
            //            (  test.foo={f1: 666})
            observe(val)
             // todo:     
          }
        }
      })
    }

    WatcherとDepの作成
    1. Watcher
  • 依存収集後depsに保存
  • が変動したときdepsはパブリッシャー
  • にパブリッシャーとして通知する.
  • watcherコールバックレンダリング
  • class Watch {
      // expOrFn:    Watcher           
      constructor (vm, expOrFn){
        this.vm = vm
        this.getter = expOrFn
        //       
        this.get()
      }
      get() {
        Dep.target = this
        this.getter.call(this.vm)
        Dep.target = null
      }
      update() {
        // Dep        
        this.get()
      }
    }

    2. Dep
  • パブリッシャー、複数のオブザーバー
  • を購読可能
  • 収集依存後1つまたは複数のwatcher
  • 変動があるとwatcher
  • に通知する
    class Dep {
      //   :       key    
      constructor (){
        //       
        this.deps = new Set()
      }
      addDep (watcher) {
        this.deps.add(watcher)
      }
      notify() {
        this.deps.forEach(watcher => watcher.update())
      }
    }

    3.関係
  • Depは、watcherインスタンスの削除および通知更新
  • を含むWatcherのセットを管理する.
  • Watcherは式を解析し、依存を収集し、数値が変化するとコールバック関数をトリガし、$watch APIおよび命令でよく使用される.各コンポーネントにも対応するWatcherがあり、数値の変化によってupdate関数がトリガーされ、
  • が再レンダリングされます.
    4.defineReactiveメソッドの改造---Depインスタンスの作成、依存の収集&サブスクリプションの配布
    function defineReactive (obj, key, val) {
        //   val   ,       
      observe(val)
      //    Dep   
      const dep = new Dep()
        Object.defineProperty(obj, key, {
        get () {
          //         
          Dep.target && dep.addDep(Dep.target)
          return val
        },
        set(newVal) {
          if (newVal !== val) {
            val = newVal
            //   newVal   ,        
            observe(val)
             //     
            dep.notify()
          }
        }
      })
    }

    テンプレートをコンパイル、$mount実装
    1.$mount作成
    function $mount (el){
        this.$el = document.createElement(el)
          //              lifeCycleMixin    _update renderMixin    _render
        const updateComponent = ()=> {
          const { render } = this.$options
          //       ,  vnode
          const vnode = render.call(this, this.$createElement)
          this._update(vnode)
        }
        //      watcher   
        new Watcher(this, updateComponent)
      }

    2.$createElementと_update実装
  • $createElementはただ一つのことをします:仮想dom($createElementは実際にrender関数に渡されるh)
  • を返します.
    function $createElement(tag, props, children) {
      return { tag, props, children}
    }
  • _update関数はdomの更新を担当し、vnodeをdom
  • に変換します.
    function _update(vnode) {
        const prevVnode = this._vnode
        if(!prevVnode) {
          //    
          this.__patch__(this.$el, vnode)
        } else {
          //   
          this.__patch__(prevVnode, vnode)
        }
      }

    3. patch
    patchはcreatePatchFunctionの戻り値であり、nodeOpsとmodulesを渡すのはwebプラットフォームが特に実現している.
    patch実装
    まず、ツリーレベルの比較を行います.3つの場合があります.削除を追加します.
  • new VNodeが存在しない場合は削除します.
  • old VNodeが存在しなければ増加する.
  • は、diff実行更新について、属性更新、テキスト更新、サブノード更新の3つのタイプのアクションを含む2つのVNodeを比較しています.
  • 新旧ノードにchildrenサブノードがある場合、サブノードに対してdiff操作を行い、**updateChildren
  • を呼び出す.
  • 新しいノードにサブノードがあり、古いノードにサブノード**がない場合は、古いノードのテキスト内容を空にしてから、サブノードを追加します.
  • 新しいノードにサブノードがなく、古いノードにサブノードがある場合、そのノードのすべてのサブノードが除去される.
  • 新しい古いノードにサブノードがない場合、テキストの置換だけです.

  • function __patch__(oldVnode, Vnode) {
        // oldVnode  dom
        if (oldVnode.nodeType) {
          const parent = oldVnode.parentElement
          const refElm = oldVnode.nextSibling
          //     dom       dom,      
          const el = this.createElm(vnode)
          parent.insertBefore(el, refElm)
          //       
          parent.removeChild(oldVnode)
        } else {
          //   dom
          const el = vnode.el = oldVnode.el
          //                 
          if (oldVnode.tag === vnode.tag) {
            const oldCh = oldVnode.children
            const newCh = vnode.children
            /**
             *      diff   
             * 1.      string (    )
             * 2.        (  diff)
             * 3.      ,    string(    dom )
             * 4.    string,       (            )
             */
            if (typeof newCh === 'string') {
              //    string
              if (typeof oldCh === 'string') {
                //     string
                if (newCh !== oldCh) {
                  el.textContent = newCh
                }
              } else {
                //    string         dom       
                el.textContent = newCh
              }
            } else {
              //      
              // 1.      ,     (        ,        dom )
              if (typeof oldCh === 'string') {
                oldCh.innerHTML = ''
                newCh.forEach(vnode => this.createElm(vnode))
              } else {
                // 2.        (      diff    )
                this.updateChildren(el, oldCh, newCh)
              }
            }
          } else {
            //             
          }
        }
        //     vnode
        this._vnode = vnode
      }

    4.updateChildrenは新旧の2つのVNodeのchildrenに対して最小の操作を出す
  • は2サイクルを実行するのが伝統的な方法であり、vueではwebシーンの特徴に対して特別なアルゴリズム最適化
  • を行った.
  • 新旧の2組のVNodeノードの左右の頭と尾の両側に変数タグがあり、遍歴中にこれらの変数が中間に近づく.oldStartIdx>oldEndIdx、またはnewStartIdx>newEndIdxの場合に終了するループ
  • の下には、遍歴ルールがあります.
  • oldStartVnodeとnewStartVnodeまたはoldEndVnodeとnewEndVnodeがsameVnodeを満たす場合、このVNodeノードを直接patchVnodeにすればよく、これ以上巡回することなく
  • サイクルを完了する.
  • oldStartVnodeとnewEndVnodeがsameVnodeを満たす場合.説明oldStartVnodeはすでにoldEndVnodeの後ろに走っており、patchVnodeを行うと同時に実際のDOMノードをoldEndVnodeの後ろ
  • に移動する必要がある.
  • oldEndVnodeとnewStartVnodeがsameVnodeを満たす場合、oldEndVnodeがoldStartVnodeの前に走ったことを説明し、patchVnodeを行うと同時にoldEndVnode対応DOMをoldStartVnode対応DOMの前に移動する.
  • 以上が一致しない場合、old VNodeでnewStartVnodeと同じノードを探し、patchVnodeが実行されている場合、elmToMoveをoldStartIdxに対応するDOMの前に移動します.
  • newStartVnodeがold VNodeノードで一致するsameVnodeが見つからない可能性もあります.このときcreateElmを呼び出して新しいDOMノードを作成します.
  • はこのサイクルで終了するが、残りのノードを処理する必要がある.
  • 終了時oldStartIdx>oldEndIdx、この時点で古いVNodeノードは遍歴済みですが、新しいノードはまだありません.新しいVNodeノードは実際に古いVNodeノードよりも多く、残りのVNode対応のDOMを実際のDOMに挿入する必要があることを説明し、addVnodes(createElmインタフェースを一括呼び出します)を呼び出します.
  • ただし、終了時にnewStartIdx>newEndIdxとなると、新しいVNodeノードは遍歴済みであるが、古いノードはまだ残っており、ドキュメントから削除するノードは
  • を削除する必要がある.


    ⚠️元のアルゴリズムは比較的に複雑で、直接ソースコードを調べて調べることができて、以下私達は1つの最適化されていない硬い更新の操作を実現することができます
    //     
      updateChildren (parentElm, odlCh, newCh) {
        const len = Math.min(oldCh.length, newCh.length)
        //         
        for(let i =0; i oldCh.length) {
          newCh.slice(len).forEach(vnode=>{
            const el = this.createElm(vnode)
            parentElm.appendChild(el)
          })
        } else if(newCh.length < old.length){
          parentElm.removeChild(vnode.el)
        }
      }

    5.createElm再帰domツリーの作成
    createElm(vnode) {
        const el = document.createElement(vnode.tag)
        //   props
        if(vnode.props) {
          for(const key in vnode.props) {
            el.setAttribute(key, vnode.props[key])
          }
        }
        //   children
        if (vnode.children) {
          //     
          if(typeof vnode.children === 'string') {
            el.textContent = vnode.children
          } else {
            //      
            vnode.children.forEach(vnode=>{
              const child = this.createElm(vnode)
              el.appendChild(child)
            })
          }
          vnode.el = el
          return el
        }
      }

    6.完全バージョンのTVueソース
    function defineReactive(obj, key, val) {
      // !       
      observe(val)
      //   Dep  
      const dep = new Dep()
      Object.defineProperty(obj, key, {
        get() {
          console.log(`get ${key}: ${val}`)
          Dep.target && dep.addDep(Dep.target)
          return val
        },
        set(newVal) {
          if (newVal !== val) {
            console.log(`set ${key}: ${newVal}`)
            val = newVal
            //!            (  test.foo={f1: 666})
            observe(val)
            dep.notify()
          }
        }
      })
    }
    function observe(obj) {
      if (typeof obj !== 'object' || obj === null) {
        return
      }
      // *   obj   ,        Observer  
      new Observer(obj)
    
    }
    function proxy(vm) {
      Object.keys(vm.$data).forEach(key => {
        Object.defineProperty(vm, key, {
          get() {
            return vm.$data[key]
          },
          set(v) {
            vm.$data[key] = v
          }
        })
      })
    }
    class Observer {
      constructor(options) {
        if (Array.isArray(options)) {
          // todo        
        } else {
          this.walk(options)
        }
      }
      walk(obj) {
        Object.keys(obj).forEach(key => {
          defineReactive(obj, key, obj[key])
        })
      }
    }
    class TVue {
      constructor(options) {
        this.$options = options
        this.$data = options.data
        // ! 1.     
        observe(this.$data)
        // ! 1.5     data         JVue         
        proxy(this)
        // ! 2.  
        // new Compile(options.el, this)
        if (options.el) {
          this.$mount(options.el)
        }
      }
      $mount (el) {
        //       
        this.$el = document.querySelector(el)
        const updateComponent = () => {
           //       
          const { render } = this.$options;
    
          //   dom     
          // const el = render.call(this);
          // const parent = this.$el.parentElement;
          // parent.insertBefore(el, this.$el.nextSibling);
          // parent.removeChild(this.$el);
          // this.$el = el;
    
          // vnode    
          const vnode = render.call(this, this.$createElement)
          this._update(vnode)
        }
        //      Watcher   
        new Watcher(this, updateComponent)
      }
      $createElement (tag, props, children) {
        return {
          tag,
          props,
          children
        }
      }
      _update (vnode) {
        const prevVnode = this._vnode
        if (!prevVnode) {
          this.__patch__(this.$el, vnode)
        } else {
          this.__patch__(prevVnode, vnode)
        }
      }
      __patch__ (oldVnode, vnode) {
        // oldVnode dom
        if (oldVnode.nodeType) {
          const parent = oldVnode.parentElement
          const refElm = oldVnode.nextSibling
          // props
          // children
          const el = this.createElm(vnode)
          parent.insertBefore(el, refElm)
          parent.removeChild(oldVnode)
        } else {
          // update
          //   dom
          const el = vnode.el = oldVnode.el
          if (oldVnode.tag === vnode.tag) {
            const oldCh = oldVnode.children
            const newCh = vnode.children
    
            /**
             *     diff
             * 1.      string (    )
             * 2.        (  diff)
             * 3.      ,    string(    dom )
             * 4.    string,       (            )
             */
            if (typeof newCh === 'string') {
              if(typeof oldCh === 'string') {
                //       string         
                if(newCh !== oldCh) {
                  el.textContent = newCh
                }
              } else {
                el.textContent = newCh
              }
    
            } else {
              // 1.      ,     (        ,        dom )
              if (typeof oldCh === 'string') {
                //     
                oldCh.innerHTML = ''
                newCh.forEach(vnode => this.createElm(vnode))
              } else {
                // 2.        
                this.updateChildren(el, oldCh, newCh)
              }
            }
          }
        }
        this._vnode = vnode
      }
      //     dom 
      createElm (vnode) {
        const el = document.createElement(vnode.tag)
        //    props
        if (vnode.props) {
          for (const key in vnode.porps) {
            el.setAttribute(key, vnode.props[key])
          }
        }
        //    children
        if (vnode.children) {
          //     
          if (typeof vnode.children === 'string') {
            el.textContent = vnode.children
          } else {
            //    
            vnode.children.forEach(vnode => {
              const child = this.createElm(vnode)
              el.appendChild(child)
            })
          }
        }
        // vnode    dom
        vnode.el = el
        return el
      }
      //     
      updateChildren(parentElm, oldCh, newCh) {
        const len = Math.min(oldCh.length, newCh.length)
        //           
        for (let i = 0; i < len; i++) {
          this.__patch__(oldCh[i], newCh[i])
        }
    
        // newCh       ,  
        if (newCh.length > oldCh.length) {
          newCh.slice(len).forEach(vnode => {
            const el = this.createElm(vnode)
            parentElm.appendChild(el)
          })
        } else if(newCh.length < oldCh.length){
          oldCh.slice(len).forEach(vnode => {
            parentElm.removeChild(vnode.el)
          })
        }
      }
    }
    
    
    //       ,       
    class Watcher {
      constructor(vm, expOrFn) {
        this.vm = vm;
        this.getter = expOrFn;
         //       
        this.get()
    
      }
      get () {
        Dep.target = this;
        this.getter.call(this.vm)
        Dep.target = null
      }
      // Dep       
      update() {
        this.get()
      }
    }
    //   :       key    
    class Dep {
      constructor() {
        this.deps = new Set();
      }
      addDep(wather) {
        this.deps.add(wather)
      }
      notify() {
        this.deps.forEach(wather => wather.update())
      }
    }

    まとめ
    以上、データ応答と非同期一括更新domインフラストラクチャを実現する簡単なバージョンのTVueを作成しました.
    もちろんVue 2.xの強さはそれだけではありません.残りはソースコードに答えを探しに行きましょう.
    以下はVueのいくつかの重要な概念の簡単な紹介で、皆さんがこの文章の全体的な考え方をもっとよく理解するのに役立つかもしれません.
  • Observerは、データにDep依存性を追加するために使用されます.
  • Depは、dataの各オブジェクトにサブオブジェクトが含まれているオブジェクトであり、バインドされたデータが変更された場合、dep.notify()によってWatcherに通知される.
  • CompileはHTMLコマンド解析器であり、各要素ノードのコマンドをスキャンして解析し、コマンドテンプレートに基づいてデータを置き換え、対応する更新関数をバインドします.
  • WatcherはObserverとCompileを接続するブリッジであり、Compileが命令を解析すると対応するWatcherが作成され、updateメソッドがバインドされ、Depオブジェクトに追加されます.

  • 拡張
    Vueソースの学習テクニック
  • 取得Vueソースコード項目アドレス:https://github.com/vuejs/vue
  • デバッグ環境構築
  • インストール依存:npm iインストールphantom.jsで
  • を終了
  • インストールrollup:npm i-g rollup devスクリプトを修正し、sourcemap、packageを追加します.json
  • 開発コマンドを実行する:npm run devは前に作成したvueを導入する.js

  • "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web- full-dev"
  • 用語の説明:
  • runtime:実行時のみ、コンパイラ
  • は含まれません.
  • common:cjs仕様、webpack 1
  • esm:ESモジュール、webpack 2+
  • umd:universal module definition、cjsとamdに対応、浏
  • に使用