實現乞丐版的 vue data + method + computed

最近有了點空,就想着 把 vue 給搞定了,

看了一遍之後,決定自己寫一個乞丐版的 vue,上班的時候划水一個早上也算是 結束了

index.html

  <div id="app">
    {{age}}
    <p>{{name}}</p>
    <p>{{name}}</p>
    <div>
      <p>
        <span @click="printName">{{name}}</span>
      </p>
      <input type="text" v-model="inp">
      {{inp}}
    </div>
  </div>
  <script src="./dcue/PoorVue.js"></script>
  <script src="./dcue/analyze.js"></script>
  <script src="./index.js"></script>

index.js

const props = {
  el: document.querySelector('#app'),
  data: {
    name: 'jack',
    age: 123,
    inp: ''
  },
  methods: {
    printName() {
      this.name = this.name + 1
    }
  }
}

new PoorVue(props)

PoorVue.js

由於當時是純手打,沒有看其他的東西,所以函數的命名上、部分代碼可能會有出入

class PoorVue {
  constructor(props) {
    // 對當前的 數據進行保存
    this.props = props;
    this.$data = props.data;
    // 在這裏對數據進行一個 雙向綁定的前期工作,也就是代理工作
    // 大名鼎鼎的  defineProperty 就是在 這裏進行的
    this.defineData(this.$data);
    // 對 html 進行解析,這裏只會 提取 {{}} 和 @ 事件
    new Analyze(this.props.el, this)
  }
  defineData(data) {
    // 判斷當前的 數據是不是一個對象
    if (Object.prototype.toString.call(data) === "[object Object]") {
      Object.keys(data).map(key => {
        // 這裏是對 data 中的數據進行一個代理
        // 這樣的話,就可以直接使用 this[props]  而不是 this.$data[props]
        this.proxydata(key)
        // 這裏真正的 對數據進行代理,並收集 watcher 了
        this.defineProperty(data, key, data[key]);
      })
    }
  }
  // 這裏是對 data 中的數據進行一個代理
  // 這樣的話,就可以直接使用 this[props]  而不是 this.$data[props]
  proxydata(key) {
    Object.defineProperty(this, key, {
      get() {
        return this.$data[key]
      },
      set(val) {
        this.$data[key] = val
      }
    })
  }
  defineProperty(obj, key, value) {
    // 對數據進行一個遍歷
    // 可以對 對象內部的 數據進行深層次的綁定
    this.defineData(value)
    // 建立一個 Dep ,也就是依賴收集工具
    // 把所有 記錄着 key 這個參數 要做的操作 都放進這個數據之中
    const dep = new Dep()

    Object.defineProperty(obj, key, {
      get() {
        // 判斷 Dep.target 是否存在,如果存在,就 進行依賴收集
        // 這樣也是爲了防止多次收集
        Dep.target && dep.addDep()
        return value
      },
      set(val) {
        // 當 數據一致的時候,不進行任何操作
        if (value === val) return
        value = val;
        // 進行響應,頁面開始進行變化
        // 這裏的 dep 實際上已經在 閉包裏面了
        // 所以也就是說,一個 key 對應 於一個 dep
        dep.notify()
      }
    })
  }
}

class Dep {
  constructor() {
    // 建立一個收集 依賴的數組
    this.deps = []
  }
  addDep() {
    // 收集 對應的 Watcher
    this.deps.push(Dep.target);
  }
  notify() {
    // 執行所有 的 Watcher
    // 注意,這裏的 Watcher 都是對應於 同一個 key 之下的
    this.deps.map(dep => dep.update())
  }
}

class Watcher {
  constructor(vm, exp, cb) {
    this.vm = vm;
    this.cb = cb;
    // 對應了 上面數據之中 的 Dep.target && dep.addDep()
    Dep.target = this;
    // 執行 一下 這個函數,
    // 也就是說 在這個過程之中
    // 先是執行了 之前就定義好的 defineProperty get 函數
    // 這樣就 能夠 將 當前的 Watcher 給收集到 Dep 當中
    // 同時 也可以對頁面進行第一步 的渲染
    this.cb.call(this.vm);
    // 將這個 置爲 null, 也就是說 當前依賴已經收集完畢
    // 爲 接下來的 Watcher 騰位置的同時
    Dep.target = null;
  }
  update() {
    // 執行當前 的 收集的依賴
    this.cb && this.cb.call(this.vm);
  }
}

analyze.js

class Analyze {
  constructor(el, vm) {
    this.$el = el;
    this.vm = vm;
    // 開始解析 當前的 html 代碼
    this.resolve(this.$el)
  }
  createFrgment(el) {
    const frg = document.createDocumentFragment();
    const childNodes = el.childNodes;
    Array.from(childNodes).map(child => {
      frg.appendChild(child)
    })
    return frg
  }
  resolve(el) {
    // 創建 Fragment,並將 當前頁面上的 所有節點放到 這裏來
    // 實際上也是爲了防止 在解析的過程中,會進行 過多的 dom 操作
    // 避免 資源的浪費
    const fragment = this.createFrgment(el);
    // 開始解析
    this.ergodic(fragment)
    this.$el.appendChild(fragment)
  }
  ergodic(frag) {
    const nodeList = frag.childNodes;
    Array.from(nodeList).map(node => {
      // 是節點類型的話,開始解析 當前節點的 屬性
      if (node.nodeType === 1) {
        const attr = node.attributes
        this.dealAttribute(node, attr)
      }
      // 是文本節點的話,看看是不是 {{}} 插值 插進去的
      if (node.nodeType === 3) {
        if (this.interP(node.textContent)) {
          // 進行當前的依賴管理
          this.update(node, RegExp.$1, 'text')
        }
      }
      // 如果當前是節點,並且還有子節點的話,繼續 解析
      if (node.childNodes && node.childNodes.length > 0) {
        this.ergodic(node)
      }
    })
  }
  dealAttribute(node) {
    Array.from(node.attributes).map(attr => {
      // 這裏寫的很簡單,就是 以  @ 開頭的屬性的話,就在當前節點進行綁定
      // 注意,vue 是綁定在 document 上,進行了 事件委託的
      if (attr.name.startsWith('@')) {
        const method = attr.value
        node.addEventListener(attr.name.substr(1), this.vm.props.methods[method].bind(this.vm))
      }
      //  這裏關於 v-model 就不做太多的校驗了,明白意思就好
      if (attr.name === 'v-model') {
        const value = attr.value
        console.log(value)
        this.update(node, value, 'model')
      }
    })
  }
  update(node, exp, type) {
    // 找到 當前處理的 函數 handler
    const handler = this[`${type}Handler`];
    const vm = this.vm
    // 創建一個 Watcher ,把 渲染的函數發過去
    new Watcher(this.vm, exp, function() {
      handler && handler(node, exp, vm)
    })
  }
  // 解析 {{}}
  // 注意,這樣解析之後 RegExp.$1 就是匹配的 結果
  interP(text) {
    return /\{\{(.*)\}\}/.test(text)
  }
  // 這個就很簡單了,在 update 之中被調用 不贅述
  textHandler(node, exp, vm) {
    node.textContent = vm.$data[exp]
  }
  // v-model 的渲染函數
  modelHandler(node, exp, vm) {
    node.value = vm[exp];
    node.addEventListener('input', (e) => {
      vm[exp] = e.target.value
    })
  }
}

所以說,在自己實現了一遍之後,倒是對於 Dep Watcher 的理解更深刻了

每一個 data 裏面的數據,包括深層次對象裏面的 數據,每一個 字段都會有一個 Dep,每一個 雙向綁定的地方,就會有一個 Watcher

然後每一個 頁面,或者 computed ,或者 watch 每一個 都會有 一個 Watcher,

例如上面,

頁面上有 兩個 {{name}}, 那麼每一個 name ,就會有一個 Watcher,然後這些 name 的 Watcher都會被放在 同一個 Dep 之中

=========

2020.3.20更新

這次在 這個 乞丐版的 vue 上增加 一個 computed 屬性

先看 對應的 文件配置,增加了 一個 fullname

<input type="text" v-model="firstname">
<input type="text" v-model="lastname">
{{fullname}}
const props = {
   el: document.querySelector('#app'),
   data: {
     name: 'jack',
     age: 123,
     inp: '',
++   firstname: '',
++   lastname: ''
   },
++ computed: {
++   fullname() {
++     return this.firstname + this.lastname
++   }
   },
   methods: {
     printName() {
       this.name = this.name + 1
     },
     reduce() {
       this.name = this.name.slice(0, -1)
     }
   }
 }

 new PoorVue(props)

由於 只是 簡單地實現,以及 瞭解其原理,就不搞那麼複雜了

PoorVue.js

class PoorVue {
  constructor(props) {
    // 對當前的 數據進行保存
    this.props = props;
    this.$data = props.data;
    // 在這裏對數據進行一個 雙向綁定的前期工作,也就是代理工作
    // 大名鼎鼎的  defineProperty 就是在 這裏進行的
    this.defineData(this.$data);
    // 對 computed 屬性進行解析
    this.defineComputed(this.props.computed);
    // 對 html 進行解析,這裏只會 提取 {{}} 和 @ 事件
    new Analyze(this.props.el, this)
  }
  ....
  ....
  // 可以看到,我其實什麼都沒有改,
  // 就是在這裏增加了這麼一段代碼,就讓 整個函數運行起來了
  defineComputed(computed) {
    const _this = this;
    Object.keys(computed).map(comput => {
      const dep = new Dep()
      Object.defineProperty(this, comput, {
        enumerable: true,
        configurable: true,
        get() {
          Dep.target && dep.addDep()
          return computed[comput].call(_this)
        },
      })
    })
  }
 
 ....
 ....
}
  1. 首先得了解的一點,在 vue 的源碼中, computed 的初始化時相對靠後的位置的,所以 在 computed 裏可以使用之前就定義好的  data ,props 屬性
  2. 這裏的 computed 屬性我只弄成了 只讀,也就是 只有 get,沒有 set
  3. 在 解析 裏解析到 computed 的屬性之後,就執行 上面定義好的 關於 computed  的 defineproperty
  4. 執行了 computed 的 get,也就是 那個函數之後,也就 立即調用了 裏面 依賴的 那兩個 data 屬性,firstname 和 lastname
  5. 這兩個屬性 在被 get 的時候,執行了 自己定義的 defineproperty
  6. 這樣的話, 在 firstname 和 lastname 裏,也就會執行 Dep.target && dep.addDep() 這段代碼
  7. Dep.target 在這個時候 指向的是 computed 裏的 fullname 的 watcher
  8. Dep.target 有值,就把 這個 watcher 分別插進了 firstname 和 lastname 的 dep 裏面
  9. 然後 fullname 的 watcher 執行完畢,Dep.target = null 被清空
  10. 這樣 當 firstname 或者 lastname 的值進行了改變的時候,就會 去 執行 computed 裏的 fullname 的 watcher,然後渲染到頁面上

 效果圖

 所以說,在這裏我們可以看到 這個 computed 屬性 fullname 的 watcher 不僅僅是進入了一個 Dep 中,還被放進了 fullname 和 lastname 的 Dep 之中

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章