Vue數據響應式和編譯原理分析 和 模擬實戰

第一步,分析

須要文件

  1. 一個本身寫的Vue.js文件 稱爲MyVue
  2. 一個模板編譯文件,把插值表達式( {{xxx}}稱之爲插值表達式 )替換成對應的值

思路分析

​ 引入MyVue文件,New一個 Vue對象,掛載元素,處理數據。響應化設置,模板編譯。javascript

全部類的功能

Vue文件

MyVue

這個很少說,主文件html

Dep

管理/控制者,管理/控制數據的更新vue

Watcher

觀察者,觀察數據的變化,寫數據更新的方法java

Compil文件

只有這一個,負責編譯node

第二步,框架搭建

MyVue

class Myvue { // 核心文件
  
}

// 管理 若干watcher 的實例,和key 是一對一
class Dep {
  
}

// 保存依賴(和key的依賴) 實現update更新
class Watcher {
  
}

複製代碼

compile

// 遍歷模板 將裏面的插值表達式作處理
// 若是發現 k-bind 等指令 特殊處理

class Compile {
  
}

複製代碼

第三步,具體實現

MyVue文件中

myvue類

所有代碼展現閉包

class myvue {
  constructor (options) {
    // 初始化 加$做用 區分開
    this.$options = options
    this.$data = options.data
    // 響應式
    this.observe(this.$data)
      
    new Compile(options.el, this)
    options.created && options.created.call(this)
  }

  // 遞歸遍歷,是數據相應化
  observe (value) {
    if (!value || typeof value !== 'object') {
      return
    }

    // 遍歷
    Object.keys(value).forEach(key => {
      // 定義響應式
      this.defineReactive(value, key, value[key])
      this.proxyData(key)
    })
  }

  // 座一層代理
  proxyData (key) {
    // 這裏的this 指的是app 實例
    Object.defineProperty(this, key, {
      get () {
        return this.$data[key]
      },
      set (newValue) {
        this.$data[key] = newValue
      }
    })
  }

  // 函數外面訪問了內部遍歷 造成了閉包 定義響應式
  defineReactive (obj, key, val) {
    // 遞歸 遍歷深對象
    this.observe(val)

    // 建立Dep Dep和key 一一對應
    const dep = new Dep() // Vue依賴收集的代碼
    Object.defineProperty(obj, key, {
      get () {
        // 將Dep 指向的watcher 放到Deo中
        Dep.target && dep.addDep(Dep.target) // Vue依賴收集的代碼
        return val
      },
      set (newValue) {
        if (newValue !== val) {
          val = newValue
          // console.log(`${key}屬性更新了`) // Vue數據響應的代碼
          dep.notify()
        }
      }
    })
  }
}
複製代碼

構造函數

constructor (options) {
    // 初始化 加$做用 區分開
    this.$options = options
    this.$data = options.data
    // 響應式
    this.observe(this.$data)
    
    // 編譯相關 後面會再說
    new Compile(options.el, this)
    options.created && options.created.call(this)
  }
複製代碼

咱們首先要接收一個 配置項options,而後保存一下並拿出來數據。app

拿出data數據後要進行 數據的響應式 也就是observe,並把data傳入。最後編譯模板框架

注,$符號僅僅做爲區分,並沒有他用dom

observe函數

// 遞歸遍歷,是數據相應化
  observe (value) {
    if (!value || typeof value !== 'object') {
      return
    }

    Object.keys(value).forEach(key => {
      // 定義響應式
      this.defineReactive(value, key, value[key])
      this.proxyData(key)
    })
  }
複製代碼

衆所周知,咱們須要的data是一個函數或者對象。這裏咱們 只考慮對象,若是不是對象直接返回。而後遍歷數據,拿到key。給value對象裏面對應的key設置響應式。最後 設置一層代理函數

代理的做用app是myvue實例

不設置代理的化,咱們須要這樣訪問數據

app.$data.test = '123456'

設置代理以後

能夠直接訪問app.test = '123456'

defineReactive函數

defineReactive (obj, key, val) {
    // 遞歸 遍歷深對象
    this.observe(val)
    // 建立Dep Dep和key 一一對應
    const dep = new Dep() // Vue依賴收集的代碼
    Object.defineProperty(obj, key, { // 給這個對象添加 訪問器屬性
      get () {
        // 將Dep 指向的watcher 放到Deo中
        Dep.target && dep.addDep(Dep.target) // Vue依賴收集的代碼
        return val
      },
      set (newValue) {
        if (newValue !== val) {
          val = newValue
          // console.log(`${key}屬性更新了`) // Vue數據響應的代碼
          dep.notify()
        }
      }
    })
  }
複製代碼

Dep類

做用:管理 若干watcher 的實例

class Dep {
  constructor () {
    this.deps = []
  }

  addDep (Watcher) {
    this.deps.push(Watcher)
  }

  // 通知 即執行watcher 裏面的 update 函數
  notify () {
    this.deps.forEach(dep => dep.update())
  }
}
複製代碼

這個類比較簡單。就不詳細說了

Watcher類

// 保存依賴(和key的依賴) 實現update更新
class Watcher {
  constructor (vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb // 後來加上的 動態改變的時候加上的

    // 把當前實例指定給Dep.target
    Dep.target = this
    this.vm[this.key] // 觸發get 動態改變的時候加上的
    Dep.target = null
  }

  update () {
    this.cb.call(this.vm, this.vm[this.key]) // 發生嵌套得不到值 保證this指向正確
  }
}
複製代碼

這個引用是再 編譯代碼的時候建立的,感受最主要的是this.vm[this.key]這句可能不太理解,就是這樣訪問這個屬性,這個就觸發了get函數,使得 添加到Dep.deps 裏面,這樣就能夠監聽到他的變化

compile文件

所有代碼展現

class Compile {
  constructor (el, vm) { // 接收一個vue 實例 和綁定元素
    this.$vm = vm 
    this.$el = document.querySelector(el) // 得到元素

    if (this.$el) {
      // 把 el裏面的內容放到另外一個fragment裏面去,也就是另外一個空白DOM樹,提升操做效率
      this.$fragment = this.node2Fragment(this.$el)
      console.log(this.$fragment)
      // 編譯 fragment
      this.compile(this.$fragment)

      // 將編譯結果追加到宿主中 有則刪除從新添加
      this.$el.appendChild(this.$fragment)
    }
  }

  // 遍歷el 將裏面的內容 搬到新建立的fragment
  node2Fragment (el) {
    const fragment = document.createDocumentFragment() // 建立空白DOM樹
    let child
    while ((child = el.firstChild)) { // 每次取出一個
      // console.log(el.firstChild.nodeName) // 文本也算一個節點
      // appendChild移動操做 即全部孩子全到了 fragment下
      fragment.appendChild(child)
    }
    // console.log(el.firstChild) 輸出結果爲空
    return fragment
  }

  // 編譯 把指令和事件作處理
  compile (el) {
    // 遍歷el
    const childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      if (this.isElement(node)) {
        // console.log(`編譯元素:${node.nodeName}`)

        // 若是是元素節點,就要處理指令等
        this.compileElement(node)
      } else if (this.isInterpolation(node)) { // 是否是插值表達式
        // console.log(`編譯文本:${node.textContent}`)
        // 處理文本
        this.compileText(node)
      }

      // 遞歸子元素
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }

  // 是否是 元素節點 參考網址 https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType
  isElement (node) {
    return node.nodeType === 1
  }

  // 插值表達式的判斷 須要知足 {{xx}}
  isInterpolation (node) {
    // test 測試方法 參考網址以下
    // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent) // textContent返回一個節點後代和文本內容
  }

  // 編譯文本
  compileText (node) {
    // console.log(RegExp.$1) // 這個就是匹配出來的值 {{xxx}} 這個就是xxx
    const exp = RegExp.$1
    this.update(node, this.$vm, exp, 'text')
    // node.textContent = this.$vm[exp]
  }

  // update函數 可複用 exp表達式 dir具體操做
  update (node, vm, exp, dir) {
    const fn = this[dir + 'Update']
    fn && fn(node, this.$vm[exp])
    new Watcher(vm, exp, function () { // 添加響應式
      fn && fn(node, vm[exp])
    })
    // 建立watcher
  }

  textUpdate (node, exp) {
    node.textContent = exp
  }

  modelUpdate (node, value) {
    node.value = value
  }

  htmlUpdate (node, value) {
    node.innerHTML = value
  }

  // 編譯元素節點
  compileElement (node) {
    // 查看 node特性中 是否有 k-xx這樣的指令
    const nodeAttrs = node.attributes // attribute屬性返回該元素全部屬性節點的一個實時集合
    // console.log(nodeAttrs)
    Array.from(nodeAttrs).forEach(attr => {
      const attrName = attr.name // k-xxx
      const exp = attr.value // k-xxx = 'abc' 這是abc
      if (attrName.indexOf('k-') === 0) {
        const dir = attrName.substring(2) // 拿到xxx
        // console.log(this)
        this[dir] && this[dir](node, this.$vm, exp)
      } else if (attrName.indexOf('@') === 0) { // 這就是事件
        const eventName = attrName.substring(1)
        this.eventHandle(node, this.$vm, exp, eventName)
      }
    })
  }

  // text指令實現
  text (node, vm, exp) {
    this.update(node, vm, exp, 'text')
  }

  // 雙向綁定實現
  model (node, vm, exp) {
    this.update(node, vm, exp, 'model')
    node.addEventListener('input', e => {
      vm[exp] = e.target.value
    })
  }

  html (node, vm, exp) {
    this.update(node, vm, exp, 'html')
  }

  eventHandle (node, vm, exp, eventName) {
    const fn = vm.$options.methods && vm.$options.methods[exp]
    if (eventName && fn) {
      node.addEventListener(eventName, fn.bind(vm))
    }
  }
}
複製代碼

重要代碼分析

node2Fragment

// 遍歷el 將裏面的內容 搬到新建立的fragment
  node2Fragment (el) {
    const fragment = document.createDocumentFragment() // 建立空白DOM樹
    let child
    while ((child = el.firstChild)) { // 每次取出一個
      // console.log(el.firstChild.nodeName) // 文本也算一個節點
      // appendChild移動操做 即全部孩子全到了 fragment下
      fragment.appendChild(child)
    }
    // console.log(el.firstChild) 輸出結果爲空
    return fragment
  }
複製代碼

document.createDocumentFragment() 這個方法是建立了新的 空dom樹(通常來講,不直接修改數據),而後進行遍歷.el.firstChild,屬於 移動操做,會自動向下走

compileElement

// 編譯元素節點
  compileElement (node) {
    // 查看 node特性中 是否有 k-xx這樣的指令
    const nodeAttrs = node.attributes // attribute屬性返回該元素全部屬性節點的一個實時集合
    // console.log(nodeAttrs)
    Array.from(nodeAttrs).forEach(attr => { // 獲取到每個屬性 進行判斷
      const attrName = attr.name // k-xxx
      const exp = attr.value // k-xxx = 'abc' 這是abc
      if (attrName.indexOf('k-') === 0) {
        const dir = attrName.substring(2) // 拿到xxx
        // console.log(this)
        this[dir] && this[dir](node, this.$vm, exp) // 若是存在就執行這個函數
      } else if (attrName.indexOf('@') === 0) { // 這是事件
        const eventName = attrName.substring(1)
        this.eventHandle(node, this.$vm, exp, eventName) // 執行事件函數
      }
    })
  }
複製代碼

compile

// 編譯 把指令和事件作處理
  compile (el) {
    // 遍歷el
    const childNodes = el.childNodes // 返回全部節點的集合
    Array.from(childNodes).forEach(node => {
      if (this.isElement(node)) {
        // console.log(`編譯元素:${node.nodeName}`)
        // 若是是元素節點,就要處理指令等
        this.compileElement(node) // 處理執行之類的操做
      } else if (this.isInterpolation(node)) { // 是否是插值表達式
        // console.log(`編譯文本:${node.textContent}`)
        // 處理文本
        this.compileText(node)
      }

      // 遞歸子元素
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }
複製代碼

第四步,所有代碼和效果展現

<!DOCTYPE html>
<html lang="en" xmlns="">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">
    <p>{{name}}</p>
    <p k-text="name"></p>
    <p>{{age}}</p>
<!-- <p>{{doubleAge}}</p>-->
    <input type="text" k-model="name">
    <button @click="changeName">呵呵</button>
    <div k-html="html"></div>
</div>
<script src='./Myvue.js'></script>
<script src='./Compile.js'></script>

<script> const app = new Kvue({ el: '#app', data: { name: 'I am test.', age: 12, html: '<button>這是一個按鈕</button>' }, created () { console.log('開始啦') setTimeout(() => { this.name = '我是測試' }, 1500) }, methods: { changeName () { this.name = '哈嘍,我是xxx' this.age = 1 } } }) </script>
</body>
</html>

複製代碼

相關文章
相關標籤/搜索