世界で最悪のVue.jsを作ろう

少し前に、Reactに関する同様の記事を公​​開しました。 ここでは、数行のコードを使用して、React.jsの小さなクローンを最初から作成しました。しかし、Reactは現代のフロントエンドの世界で唯一のツールではなく、Vue.jsは急速に人気を集めています。このフレームワークがどのように機能するかを見て、教育目的でVue.jsに似たプリミティブクローンを作成しましょう。



反応性



React.jsと同様に、Vueはリアクティブです。つまり、アプリケーションの状態に対するすべての変更が自動的にDOMに反映されます。ただし、Reactとは異なり、Vueはレンダリング時に依存関係を追跡し、「比較」なしで関連部分のみを更新します。



Vue.jsの反応性の鍵はメソッド Object.defineProperty



です。これにより、オブジェクトフィールドでカスタムのgetter / setterメソッドを指定し、そのフィールドへのすべてのアクセスをインターセプトできます。



const obj = {a: 1};
Object.defineProperty(obj, 'a', {
  get() { return 42; },
  set(val) { console.log('you want to set "a" to', val); }
});
console.log(obj.a); // prints '42'
obj.a = 100;        // prints 'you want to set "a" to 100'
      
      





これにより、特定のプロパティがいつアクセスされているか、またはいつ変更されているかを判断し、プロパティが変更された後にすべての依存式を再評価できます。





Vue.jsを使用すると、ディレクティブを使用してJavaScript式をDOMノード属性にバインドできます。たとえば <div v-text="s.toUpperCase()"></div>



、div内のテキストを大文字の変数値に設定します s







などの文字列を評価する最も簡単な方法 s.toUpperCase()



は、を使用すること eval()



です。evalは安全なソリューションとは見なされていませんでしたが、関数でラップしてカスタムグローバルコンテキストを渡すことで、少し改善することができます。



const call = (expr, ctx) =>
  new Function(`with(this){${`return ${expr}`}}`).bind(ctx)();

call('2+3', null);                    // returns 5
call('a+1', {a:42});                  // returns 43
call('s.toUpperCase()', {s:'hello'}); // returns "HELLO"
      
      





これはネイティブのeval



ものよりも少し安全であり、 私たちが構築している単純なフレームワークには十分です。



プロキシ



これで、を使用Object.defineProperty



してデータオブジェクトの各プロパティをラップでき ます。call()



任意の式を評価し、式が直接または間接的にアクセスしたプロパティを判別するために使用できます また、変数の1つが変更されたため、式をいつ再評価する必要があるかを判断できる必要があります。



const data = {a: 1, b: 2, c: 3, d: 'foo'}; // Data model
const vars = {}; // List of variables used by expression
// Wrap data fields into a proxy that monitors all access
for (const name in data) {
  let prop = data[name];
  Object.defineProperty(data, name, {
    get() {
      vars[name] = true; // variable has been accessed
      return prop;
    },
    set(val) {
      prop = val;
      if (vars[name]) {
        console.log('Re-evaluate:', name, 'changed');
      }
    }
  });
}
// Call our expression
call('(a+c)*2', data);
console.log(vars); // {"a": true, "c": true} -- these two variables have been accessed
data.a = 5;  // Prints "Re-evaluate: a changed"
data.b = 7;  // Prints nothing, this variable does not affect the expression
data.c = 11; // Prints "Re-evaluate: c changed"
data.d = 13; // Prints nothing.
      
      





指令



これで、任意の式を評価し、特定のデータ変数が変更されたときに評価する式を追跡できます。残っているのは、DOMノードの特定のプロパティに式を割り当て、データが変更されたときに実際にそれらを変更することだけです。



Vue.jsと同様に、q-on:click



イベントハンドラーのq-text



バインド、textContentの q-bind:style



バインド、CSSスタイルのバインド などの特別な属性を使用 します。 「q」は「vue」に似ているため、ここでは「q-」プレフィックスを使用します。



サポートされている可能性のあるディレクティブのリストの一部を次に示します。



const directives = {
  // Bind innerText to an expression value
  text: (el, _, val, ctx) => (el.innerText = call(val, ctx)),
  // Bind event listener
  on: (el, name, val, ctx) => (el[`on${name}`] = () => call(val, ctx)),
  // Bind node attribute to an expression value
  bind: (el, name, value, ctx) => el.setAttribute(name, call(value, ctx)),
};
      
      





各ディレクティブは、DOMノードを受け取る関数であり、q-on:click



(名前は「クリック」になります)などの場合のオプションのパラメーター名です またvalue



、式のコンテキストとして使用する式文字列()とデータオブジェクトも必要 です。



すべてのビルディングブロックができたので、今度はすべてを接着します。



最終結果



const call = ....       // Our "safe" expression evaluator
const directives = .... // Our supported directives

// Currently evaluated directive, proxy uses it as a dependency
// of the individual variables accessed during directive evaluation
let $dep;

// A function to iterate over DOM node and its child nodes, scanning all
// attributes and binding them as directives if needed
const walk = (node, q) => {
  // Iterate node attributes
  for (const {name, value} of node.attributes) {
    if (name.startsWith('q-')) {
      const [directive, event] = name.substring(2).split(':');
      const d = directives[directive];
      // Set $dep to re-evaluate this directive
      $dep = () => d(node, event, value, q);
      // Evaluate directive for the first time
      $dep();
      // And clear $dep after we are done
      $dep = undefined;
    }
  }
  // Walk through child nodes
  for (const child of node.children) {
    walk(child, q);
  }
};

// Proxy uses Object.defineProperty to intercept access to
// all `q` data object properties.
const proxy = q => {
  const deps = {}; // Dependent directives of the given data object
  for (const name in q) {
    deps[name] = []; // Dependent directives of the given property
    let prop = q[name];
    Object.defineProperty(q, name, {
      get() {
        if ($dep) {
          // Property has been accessed.
          // Add current directive to the dependency list.
          deps[name].push($dep);
        }
        return prop;
      },
      set(value) { prop = value; },
    });
  }
  return q;
};

// Main entry point: apply data object "q" to the DOM tree at root "el".
const Q = (el, q) => walk(el, proxy(q));

      
      





最高のリアクティブなVue.jsのようなフレームワーク。それはどれくらい役に立ちますか?次に例を示します。



<div id="counter">
  <button q-on:click="clicks++">Click me</button>
  <button q-on:click="clicks=0">Reset</button>
  <p q-text="`Clicked ${clicks} times`"></p>
</div>

Q(counter, {clicks: 0});
      
      





1つのボタンを押すと、カウンターがインクリメントされ、コンテンツが自動的に更新されます <p>



別のものをクリックすると、カウンターがゼロに設定され、テキストも更新されます。



ご覧のとおり、Vue.jsは一見魔法のように見えますが、内部は非常にシンプルで、基本的な機能はほんの数行のコードで実装できます。



さらなるステップ



Vue.jsについて詳しく知りたい場合は、「q-if」を実装して式に基づいて要素の表示を切り替えるか、「q-each」を実装して重複する子のリストをバインドしてみてください(これは良い演習になります) )。



Q nanoframeworkの完全なソースは、 Githubにあります。問題を見つけたり、改善を提案したい場合は、遠慮なく寄付してください。



結論として、私は言及すべき Object.defineProperty



ヴュー2 Vueの3で使用したとクリエイターが、すなわち、ES6設けられた他の施設に切り替えている Proxy



Reflect



..。プロキシを使用すると、例のようにオブジェクトプロパティへのアクセスをインターセプトするハンドラーを渡すことができますが、リフレクトを使用すると、プロキシ内からオブジェクトプロパティにアクセスして、this



オブジェクトをそのまま保持できます (definePropertyを使用した例とは異なります)。



私はプロキシとリフレクトの両方を読者の練習問題として残しているので、Qでそれらを正しく使用するように要求した人は誰でも、それ を組み合わせて喜んでいます。がんばろう!



あなたが記事を楽しんだことを望みます。ニュースをフォローし、GithubTwitterで提案を共有するrssを介してサブスクライブ できます



All Articles