何以解憂,唯有自己寫(xiě)Vue

雙向綁定

Vue 用著最舒服的地方,想必就是它的雙向綁定機(jī)制,不過(guò)在搞清楚這方點(diǎn)之前,舉個(gè)最簡(jiǎn)單的例子回顧下 Vue 雙向綁定的表象。

Vue

無(wú)論是從輸入框輸入,還是從 Vue 開(kāi)發(fā)工具中編輯,都會(huì)發(fā)現(xiàn)數(shù)據(jù)和頁(yè)面展示是同步的,而 Vue 實(shí)時(shí)監(jiān)聽(tīng)頁(yè)面和數(shù)據(jù)的變化,打通雙向通道的核心,其實(shí)只是 defineProperty 這一個(gè) js 原生方法。

那就先來(lái)說(shuō)說(shuō)這個(gè) defineProperty 的功能:

1.可以直接在一個(gè)對(duì)象上定義一個(gè)新屬性。
2.可以直接修改一個(gè)對(duì)象的現(xiàn)有屬性。

Object.defineProperty(object, prop, descriptor)共有三個(gè)參數(shù):

object:需要操作的對(duì)象。
prop:需要操作的屬性名。
descriptor:屬性描述符,描述操作屬性取值,以及是否可以修改、刪除、迭代。

比如定義一個(gè) author 對(duì)象,希望可以動(dòng)態(tài)新增一個(gè) name 屬性,值為書(shū)雁祉,就可以使用 defineProperty。

let author = {};
Object.defineProperty(author, "name", {
  value: "書(shū)雁祉",
  writable: false,
  configurable: false,
  enumerable: false,
});

value:指定屬性值。
writable:指定屬性值是否可修改,默認(rèn)值為 false 不可修改。
configurable:指定屬性是否可刪除,默認(rèn)值為 false 不可刪除。
enumerable:指定屬性是否可迭代,默認(rèn)值為 false 不可迭代。

Console

除此之外,defineProperty 還可以傳入 get、set 方法用來(lái)監(jiān)聽(tīng)屬性,在獲取 / 修改對(duì)象的屬性值時(shí),會(huì)自動(dòng)調(diào)用 get / set 方法,但傳入 get、set 時(shí),不可傳入 value、writable。

let author = { name: "書(shū)雁祉", age: 14 };
for (let key in author) {
  let syz = author[key];
  Object.defineProperty(author, key, {
    configurable: false,
    enumerable: false,
    get() {
      console.log("get");
      return syz;
    },
    set(value) {
      if (value !== syz) {
        console.log("set");
        syz = value;
      }
    },
  });
}
Console

在 Vue 中,為方便管理代碼,Vue 使用了一個(gè) Observe 類(lèi)專(zhuān)門(mén)來(lái)處理對(duì)象的監(jiān)聽(tīng),在初始化類(lèi)時(shí)將需要監(jiān)聽(tīng)的對(duì)象傳入即可。defineRecative 中,this.observer(old)是考慮到對(duì)象的屬性值也可能是對(duì)象,this.observer(value)則是考慮到賦值時(shí)可能賦值一個(gè)對(duì)象,這兩種情況都需要監(jiān)聽(tīng)。

class Observer {
  constructor(data) {
    this.observer(data);
  }
  observer(object) {
    if (object && typeof object === "object") {
      for (let key in object) {
        this.defineRecative(object, key, object[key]);
      }
    }
  }
  defineRecative(object, attr, old) {
    this.observer(old);
    Object.defineProperty(object, attr, {
      get() {
        console.log("get");
        return old;
      },
      set: (value) => {
        if (value !== old) {
          console.log("set");
          this.observer(value);
          old = value;
        }
      },
    });
  }
}
let author = {
  name: {
    family: "書(shū)",
    first: "雁祉",
  },
  pet: "neko",
};
new Observer(author);
Console

回到創(chuàng)建 Vue 實(shí)例的代碼,由此可以分析出:

1.Vue 是一個(gè)類(lèi),構(gòu)造函數(shù)接收一個(gè)對(duì)象。
2.類(lèi)會(huì)將 el 指定的 dom 元素作為根節(jié)點(diǎn),將 data 中的屬性值編譯渲染到該節(jié)點(diǎn)上。
3.el 可以使 id 名,也可以是 dom 元素。
4.vue 會(huì)將 dom 與 data 分別綁定到$el 和$data 上

<div id="app">
  <input type="text" v-model="name" />
  <p>{{ name }}</p>
</div>
<script>
  const vue = new Vue({
    el: "#app",
    data: {
      name: "書(shū)雁祉",
      age: 14,
    },
  });
  console.log(vue.$el);
  console.log(vue.$data);
</script>
Console

由此,定義一個(gè) Sue 類(lèi)來(lái)實(shí)現(xiàn)與 Vue 相似的效果。

1.首先將 dom 元素和 data 都綁定在實(shí)例的$el 和$data 上,nodeType === 1 代表 el 為 dom 元素。
2.如果 dom 元素存在,根據(jù)指定的 dom 節(jié)點(diǎn),定義 Compier 編譯類(lèi),利用 data 渲染節(jié)點(diǎn)。

class Sue {
  constructor(options) {
    if (options.el.nodeType === 1) {
      this.$el = options.el;
    } else {
      this.$el = document.getElementById(options.el);
    }
    this.$data = options.data;
    if (this.$el) {
      new Compiler(this);
    }
  }
}
class Compiler {
  constructor(vm) {
    this.vm = vm;
  }
}

在寫(xiě)編譯類(lèi)前,需要了解一點(diǎn),Vue 在解析節(jié)點(diǎn)時(shí),不會(huì)每解析到一個(gè)需要渲染 data 的位置就更新一遍 dom 樹(shù),dom樹(shù)更新會(huì)十分耗時(shí)且耗費(fèi)瀏覽器性能,Vue 會(huì)將 dom 樹(shù)放在內(nèi)存中,將 data 渲染到內(nèi)存中的虛擬 dom 樹(shù)上,再將虛擬 dom 一次性渲染到界面中。
那么如何構(gòu)建虛擬dom樹(shù),方法其實(shí)有很多,Vue 才用了 js 內(nèi)置的 DocumentFragment 來(lái)構(gòu)建。

class Compiler {
  constructor(vm) {
    this.vm = vm;
    const fragment = this.createFragment(vm.$el);
    console.log(fragment);
  }
  createFragment(app) {
    const fragment = document.createDocumentFragment();
    const node = app.firstChild;
    while (node) {
      fragment.appendChild(node);
      node = app.firstChild;
    }
    return fragment;
  }
}

注意:DocumentFragment 調(diào)用 appendChild 后,該 dom 元素會(huì)從 dom 節(jié)點(diǎn)中消失,所以取 firstChild 即可取到第一個(gè)。

<div id="app">
  <input type="text" v-model="name" />
  <p>{{ name }}</p>
</div>
<script>
  const sue = new Sue({
    el: "#app",
    data: {
      name: "書(shū)雁祉",
      age: 14,
    },
  });
  console.log(sue.$el);
  console.log(sue.$data);
</script>
Console

可見(jiàn),虛擬 dom 已經(jīng)寫(xiě)入內(nèi)存,sue 實(shí)例上的 el 和data 也分別掛載了 dom 元素和 data。下一步則需要解析解析虛擬 dom 樹(shù),渲染 $data 中的數(shù)據(jù)。

1.遍歷虛擬 dom,判斷當(dāng)前遍歷到的是元素節(jié)點(diǎn)還是文本節(jié)點(diǎn)。
2.元素節(jié)點(diǎn)則需獲取屬性名和屬性值,判斷有沒(méi)有 v-* 屬性(如 v-model),同時(shí)將節(jié)點(diǎn)繼續(xù)傳入 buildFragment 處理子節(jié)點(diǎn)。
3.文本節(jié)點(diǎn)則需判斷內(nèi)容有沒(méi)有 {{}}。

class Compiler {
  constructor(vm) {
    this.vm = vm;
    const fragment = this.createFragment(vm.$el);
    this.buildFragment(fragment);
  }
  createFragment(app) {
    const fragment = document.createDocumentFragment();
    let node = app.firstChild;
    while (node) {
      fragment.appendChild(node);
      node = app.firstChild;
    }
    return fragment;
  }
  buildFragment(fragment) {
    for (let node of fragment.childNodes) {
      if (node.nodeType === 1) {
        this.buildElement(node);
        this.buildFragment(node);
      } else {
        this.buildText(node);
      }
    }
  }
  buildElement(node) {
    for (let attr of node.attributes) {
      let { name, value } = attr;
      if (name.startsWith("v-")) {
        console.log("元素", node, attr, name, value);
      }
    }
  }
  buildText(node) {
    let content = node.textContent;
    const regexp = /\{\{.+?\}\}/gi;
    if (regexp.test(content)) {
      console.log("文本", content);
    }
  }
}
Console

考慮到 v-* 屬性的多樣性,Vue 才用了一個(gè)專(zhuān)門(mén)用來(lái)處理指令的 CompilerUtil 對(duì)象,包含所有需要處理的屬性對(duì)贏得方法,這里取其中幾種舉例。

1.對(duì)于 v-model,通常用在 input 中,所以只需設(shè)置節(jié)點(diǎn)的 value 為對(duì)應(yīng) $data 的屬性值,最后在 constructor 中將 fragment 渲染到頁(yè)面即可。
2.但只是這樣會(huì)出現(xiàn)問(wèn)題,比如對(duì)應(yīng) $data 的屬性值是一個(gè)對(duì)象,而渲染的是該對(duì)象的屬性值,則無(wú)法正確獲取到,所以在 CompilerUtil 中定義 GetValue 函數(shù),專(zhuān)門(mén)用來(lái)獲取對(duì)應(yīng)的屬性值。

const CompilerUtil = {
  GetValue(value, vm) {
    const realValue = value.split(".").reduce((data, key) => {
      console.log("GetValue", data, key);
      return data[key];
    }, vm.$data);
    return realValue;
  },
  model(node, value, vm) {
    node.value = this.GetValue(value, vm);
  },
};

class Compiler {
  constructor(vm) {
    this.vm = vm;
    const fragment = this.createFragment(vm.$el);
    this.buildFragment(fragment);
    vm.$el.appendChild(fragment);
  }
  createFragment(app) {
    const fragment = document.createDocumentFragment();
    let node = app.firstChild;
    while (node) {
      fragment.appendChild(node);
      node = app.firstChild;
    }
    return fragment;
  }
  buildFragment(fragment) {
    for (let node of fragment.childNodes) {
      if (node.nodeType === 1) {
        this.buildElement(node);
        this.buildFragment(node);
      } else {
        this.buildText(node);
      }
    }
  }
  buildElement(node) {
    for (let attr of node.attributes) {
      let { name, value } = attr;
      if (name.startsWith("v-")) {
        const directive = name.split("-")[1];
        CompilerUtil[directive](node, value, this.vm);
      }
    }
  }
  buildText(node) {
    let content = node.textContent;
    const regexp = /\{\{.+?\}\}/gi;
    if (regexp.test(content)) {
      console.log("文本", content);
    }
  }
}
<div id="app">
  <input type="text" v-model="name" />
  <input type="text" v-model="time.h" />
  <p>{{ name }}</p>
</div>
<script>
  const vue = new Sue({
    el: "#app",
    data: {
      name: "書(shū)雁祉",
      age: 14,
      time: {
        h: 10,
        m: 20,
      },
    },
  });
</script>
Console

再用 v-html 和 v-text 示范,原理相似,分別修改元素的 innerHTML 和 innerText 為對(duì)應(yīng)屬性值。

const CompilerUtil = {
  GetValue(value, vm) {
    const realValue = value
      .split(".")
      .reduce((data, key) => data[key], vm.$data);
    return realValue;
  },
  model(node, value, vm) {
    node.value = this.GetValue(value, vm);
  },
  html(node, value, vm) {
    node.innerHTML = this.GetValue(value, vm);
  },
  text(node, value, vm) {
    node.innerText = this.GetValue(value, vm);
  },
};
<div id="app">
  <input type="text" v-model="name" />
  <input type="text" v-model="time.h" />
  <p>{{ name }}</p>
  <p v-html="html"></p>
  <p v-text="html"></p>
</div>
<script>
  const vue = new Sue({
    el: "#app",
    data: {
      name: "書(shū)雁祉",
      age: 14,
      time: {
        h: 10,
        m: 20,
      },
      html: "<div>a div</div>",
    },
  });
</script>
Console

Part 1就先示范這么多,等有閑情逸致了再寫(xiě)Part 2。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容