實現一個mini vue


 

Vue源碼中實現依賴收集(觀察者模式),實現了三個類:

  1. Dep:扮演觀察目標的角色,每一個數據都會有Dep類實例,它內部有個subs隊列,subs就是subscribers的意思,保存着依賴本數據的觀察者,當本數據變更時,調用dep.notify()通知觀察者
  2. Watcher:扮演觀察者的角色,進行觀察者函數的包裝處理。如render()函數,會被進行包裝成一個Watcher實例
  3. Observer:輔助的可觀測類,數組/對象通過它的轉化,可成為可觀測數據

 

index.html

 

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <div @click="getValue">
      <p k-text="inputData"></p>
    </div>
    <p @click="getValue">阿斯加德八級考試{{ form.test }}你很快就發{{ form.test }}{{ obj.a.c }}</p>
    <p>{{ inputData }}</p>
    <input :value="inputData" @input="setInput" />
  </div>
</body>
<!-- 瀏覽器不支持es6語法 -->
<script src="./kvue.js"></script>
<script>
  new Kvue({
    el: 'app',
    data: {
      form: {
        test: '1124dsa'
      },
      inputData: '',
      test: 111,
      obj: {
        a:{
          c:3
        }
      }
    },
    watch: {
      inputData(val) {
        console.log(val)
      }
    },
    created(){
      console.log(this.test)
      console.log(this.obj)
    },
    methods: {
      getValue() {
        console.log(this)
      },
      setInput(event) {
        this.inputData =  event.target.value
      }
    }
  })
</script>
</html>

kvue.js

function getData(data, vm) {
  return data.call(vm, vm)
}
// 多層對象時獲取對象的值
function parsePath (path) {
  const segments = path.split('.');
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) { return }
      obj = obj[segments[i]]
    }
    return obj
  }
}
function initMethods(vm, methods) {
  for(let key in methods) {
    vm[key] = typeof methods[key] !== 'function' ? function(){} : methods[key].bind(vm)
  }
}

function initWatch(vm, watch) {
  for(let key in watch) {
    new Watcher(vm, key, watch[key])
  }
}
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/
class Kvue{
  constructor(option) {
    // 初始化data
    let data = option.data
    data = this.$data = typeof data === 'function'
    ? getData(data, this)
    : data || {}
    const keys = Object.keys(data)
    var i = keys.length
    while(i--) {
      this.proxyData(keys[i])
    }
    this.observe(this.$data)
    // 初始化watch
    initWatch(this, option.watch)
    // 初始化methods
    initMethods(this, option.methods)
    // 模板編譯-
    new Compile(this,option.el)
    // 生命周期
    if(option.created){
      option.created.call(this)
    }
  }
  // 觀察者
  observe(obj) {
    // 判斷是否符合標准,有值並且是個對象,如果不是對象則不進行遍歷操作
    if(!obj || Object.prototype.toString.call(obj) !== '[object Object]') return
    // 遍歷obj對象的屬性,獲取屬性值后執行數據響應化處理
    Object.keys(obj).forEach((key) => {
      // 響應化處理
      this.defineProperty(obj, key, obj[key])
    })
  }
  defineProperty(obj,key,val) {
    // 調用觀察者,如果val是對象會再執行遍歷對象屬性值的操作
    this.observe(val)
    // 對象的每個屬性都會執行defineProperty方法,也意味着每個屬性都有一個dep實例,
    // 用addDep來收集這個屬性在使用的時候的Dep.tergat(前提是Dep.tergat有值)
    // 使用數組是因為一個屬性在模板中可能有多個地方都在用
    const dep = new Dep()
    Object.defineProperty(obj, key, {
      get(){
        // 依賴收集-收集 Watcher
        Dep.tergat&&dep.addDep(Dep.tergat)
        return val
      },
      set(newVal) {
        if(newVal === val) {
          return
        }
        val = newVal
        // 當數據發生變化時執行
        dep.notify()
      }
    })
  }
  // 代理函數-一次賦值就可以影響this[key]和this.$data[key],也不會影響this.$data綁定的依賴
  proxyData(key) {
    Object.defineProperty(this, key, {
      get() {
        return this.$data[key]
      },
      set(newVal) {
        this.$data[key] = newVal
      }
    })
  }
}
class Dep {
  constructor() {
    // deps用來儲存watcher
    this.deps = []
  }
  addDep(dep) {
    this.deps.push(dep)
  }
  notify() {
    // 更新watcher方法
    this.deps.forEach((watcher) => {
      watcher&&watcher.update()
    })
  }
}

// 模板編譯的時候會獲取watcher
class Watcher {
  constructor(vm,key,cb) {
    this.vm = vm
    this.key = key
    // 將Dep.tergat綁定上watcher
    Dep.tergat = this
    // 獲取this.vm[key]的時候會執行key的get方法,從而將Watcher收集到deps
    parsePath(this.key)(this.vm)
    // 回調方法用來更新模板內容
    this.cb = cb
    // 初始化更新
    this.update()
  }
  update() {
    this.cb.call(this.vm, parsePath(this.key)(this.vm))
  }
}

class Compile {
  // vm是指vue的this,el用來獲取html數據
  constructor(vm, el) {
    this.$vm = vm
    this.$el = document.getElementById(el)
    // 如果存在$el節點
    if(this.$el) {
      this.$fragment = this.nodeFragment(this.$el)
      // 執行編譯
      this.compile(this.$fragment)
      // 將編譯后的元素添加到el
      this.$el.appendChild(this.$fragment)
    }
  }
  nodeFragment(el) {
    // DocumentFragment節點不屬於文檔樹,繼承的parentNode屬性總是null。
    // 它有一個很實用的特點,當請求把一個DocumentFragment節點插入文檔樹時,插入的不是DocumentFragment自身,而是它的所有子孫節點,即插入的是括號里的節點。
    // 這個特性使得DocumentFragment成了占位符,暫時存放那些一次插入文檔的節點。它還有利於實現文檔的剪切、復制和粘貼操作。 
    // 另外,當需要添加多個dom元素時,如果先將這些元素添加到DocumentFragment中,再統一將DocumentFragment添加到頁面,會減少頁面渲染dom的次數,效率會明顯提升。
    // 如果使用appendChid方法將原dom樹中的節點添加到DocumentFragment中時,會刪除原來的節點

    // 創建一個虛擬的節點對象
    const frag = document.createDocumentFragment()
    // 將el的子元素添加到createDocumentFragment節點
    let child
    while (child = el.firstChild) {
      // 使用appendChid方法在向frag添加子元素的同時刪除了el的子元素
      frag.appendChild(child)
    }
    return frag
  }
  compile(frag) {
    const nodes = frag.childNodes || []
    // Array.from將nodelist轉為可以循環的數組
    Array.from(nodes).forEach((node, index) => {
      // html文檔中的回車空格等也是一個node節點(#text)
      // console.log(frag,node,node.nodeType)
      if(this.isElement(node)) {
        // 如果是一個元素節點則獲取它的attributes,根據attributes來獲取指令和方法綁定等
        const attributes = node.attributes
        Array.from(attributes).forEach((attr) => {
          const name = attr.name
          const value = attr.value
          if(this.isDirective(name)){
            // 獲取指令名稱
            const directive = name.substring(2)
            // 如果存在這個指令,則執行這個指令
            this[directive] && this[directive](node, this.$vm, value)
          }
          if(this.isEvent(name)) {
            // 指定事件名。
            const event = name.substring(1)
            this.eventHandler(node, this.$vm, event, value)
          }
        })
      }
      if(this.isTextNode(node)) {
        this.textNode(node, this.$vm)
      }

      if(node.childNodes){
        // 遞歸
        this.compile(node)
      }
    })
  }
  update(node, vm, exp, type) {
    const updateFn = this[`update${type}`]
    // 依賴綁定
    new Watcher(vm,exp,function(value){
      updateFn&&updateFn(node,value)
    })
  }
  // nodeType 屬性返回以數字值返回指定節點的節點類型
  // Node.ELEMENT_NODE	1	一個 元素 節點,例如 <p> 和 <div>。
  // Node.TEXT_NODE	3	Element 或者 Attr 中實際的  文字
  // Node.CDATA_SECTION_NODE	4	一個 CDATASection,例如 <!CDATA[[ … ]]>。
  // Node.PROCESSING_INSTRUCTION_NODE	7	一個用於XML文檔的 ProcessingInstruction ,例如 <?xml-stylesheet ... ?> 聲明。
  // Node.COMMENT_NODE	8	一個 Comment 節點。
  // Node.DOCUMENT_NODE	9	一個 Document 節點。
  // Node.DOCUMENT_TYPE_NODE	10	描述文檔類型的 DocumentType 節點。例如 <!DOCTYPE html>  就是用於 HTML5 的。
  // Node.DOCUMENT_FRAGMENT_NODE	11	一個 DocumentFragment 節點

  // 是否是元素節點
  isElement(node) {
    return node.nodeType === 1
  }
  isTextNode(node) {
    return node.nodeType === 3
  }
  // 是否是指令,以k-開頭
  isDirective(attrName) {
    return attrName.startsWith('k-')
  }
  // 是否是方法
  isEvent(attrName) {
    return attrName.startsWith('@')
  }
  /*
  * @作用: text指令函數
  * @params: node 操作的節點
  * @params: vm   kvue的實例
  * @params: exp 節點的屬性value值(以此來綁定對應的kvue的data)
  */
  text(node, vm, exp) {
    // 綁定更新方法
    this.update(node, vm, exp, 'Text')
  }
  textNode(node, vm) {
    const execs = defaultTagRE.exec(node.textContent)
    if(execs){
        const exp = execs[1].trimStart().trimEnd()
        this.update(node, vm, exp, 'TextNode')
        // 有多個{{}}時需要進行遞歸修改
        this.textNode(node, vm)
    }
  }
  // 文本指令更新方法
  updateText(node, value) {
    node.textContent = value
  }
   // 更新文本節點信息
  updateTextNode(node, value) {
    const textContent = node.textContent
    if(textContent) {
      node.textContent = textContent.replace(defaultTagRE, value)
    }
  }
  // 綁定方法
  eventHandler(node, vm, event, exp) {
    const fn =  vm[exp]
    node.addEventListener(event,fn)
  }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM