Vue如何实现数据双向绑定?

Vue如何实现数据双向绑定?

Vue实现思路:

Vue如何实现数据双向绑定?_第1张图片

说明:

  • 1. 实现一个Compiler模板解析器,能够对模板中的指令和插值表达式进行解析,并且赋予不同的操作

  • 2. 实现一个Observer数据监听器,能够对数据对象的所有属性进行监听

  • 3. 实现一个Watcher观察者,将Compiler的解析结果,与Observer所观察的对象连接起来,建立关系,在Observer观察到对象数据变化时,接收通知,同时更新DOM

  • 4. 创建一个公共的入口对象,接收初始化的配置并且协调上面三个模块,也就是vue.js

简化逻辑代码

index.html:


<html lang="en">
<head>
head>
<body>
  <div id="app">
    <p>{{msg}}p>
    <p>{{car.brand}}p>
    <p v-text="msg">p>
    <p v-text="car.color">p>
    <p v-html="msg">p>
    <p v-html="car.color">p>
    <input type="text" v-model="msg">
    <button v-on:click="clickFn">点我button>
  div>
  <script src="./src/watcher.js">script>
  <script src="./src/observe.js">script>
  <script src="./src/compile.js">script>
  <script src="./src/vue.js">script>
body>
html>
<script>
  const vm = new Vue({
    el: '#app',
    data: {
      msg: 'hello wrold',
      car: {
        brand: '宝马',
        color: 'blue'
      }
    },
    methods: {
      clickFn () {
        // 在vue的methods中this应该指向当前实例
        this.msg = '嘿嘿'
        this.car.brand = '奔驰'
      }
    }
  })
script>

compiler.js:

/**
 * 专门负责解析模板内容
 */
class Compile {
  constructor (el, vm) {
    // el: new vue传递的选择器
    this.el = typeof el === "string" ? document.querySelector(el) : el
    // vm: new的vue实例
    this.vm = vm

    // 编译模式
    if (this.el) {
      // 1. 把el中所有的子节点都放入到内存中, fragment
      let fragment = this.node2fragment(this.el)
      // 2. 在内存中编译fragment
      this.compile(fragment)
      // 3. 把fragment一次性的添加到页面
      this.el.appendChild(fragment)
    }
  }
  /**
   * 核心方法
   */
  node2fragment (node) {
    let fragment = document.createDocumentFragment()
    // 把el中所有的子节点挨个添加到文档碎片中
    let childNodes = node.childNodes
    this.toArray(childNodes).forEach(node => {
      // 把所有的子节点都添加到fragment中
      fragment.appendChild(node)
    })
    return fragment
  }

  /**
   * 编译文档碎片(内存中)
   * @param {*} fragment
   */
  compile(fragment) {
    let childNodes = fragment.childNodes
    this.toArray(childNodes).forEach(node => {
      // 编译子节点
      if(this.isElementNode(node)) {
        // 如果是元素,需要解析指令
        this.compileElement(node)
      }

      if (this.isTextNode(node)) {
        // 如果是文本节点,需要解析插值表达式
        this.compileText(node)
      }

      // 如果当前节点还有子节点,需要递归的解析
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }
  // 解析html标签
  compileElement(node) {
    // 1. 获取到当前节点下所有的属性
    let attributes = node.attributes
    this.toArray(attributes).forEach(attr => {
      // 2. 解析vue的指令(所有以v-开头的属性)
      let attrName = attr.name

      if(this.isDirective(attrName)) {
        let type = attrName.slice(2)
        let expr = attr.value
        
        // 解析v-on指令
        if (this.isEventDirective(type)) {
          CompileUtil["eventHandler"](node, this.vm, type, expr)
        } else {
          CompileUtil[type] && CompileUtil[type](node, this.vm, expr)
        }
      }
    })
  }

  // 解析文本节点
  compileText (node) {
    CompileUtil.mustache(node, this.vm)
  }

  /**
   * 工具方法
   */
  toArray (likeArray) {
    return [].slice.call(likeArray)
  }
  isElementNode(node) {
    // nodeType: 节点类型 1:元素节点 3: 文本节点
    return node.nodeType === 1
  }
  isTextNode(node) {
    return node.nodeType === 3
  }
  isDirective(attrName) {
    return attrName.startsWith("v-")
  }
  isEventDirective(type) {
    return type.split(":")[0] === "on"
  }

}
let CompileUtil = {
  mustache (node, vm) {
    let txt = node.textContent
    let reg = /\{\{(.+)\}\}/
    if (reg.test(txt)) {
      let expr = RegExp.$1
      node.textContent = txt.replace(reg, this.getVMValue(vm, expr))

      new Watcher(vm, expr, newValue => {
        node.textContent = txt.replace(reg, newValue)
      })
    }
  },
  // 处理v-text指令
  text(node, vm, expr) {
    node.textContent = this.getVMValue(vm, expr)
    // 通过watcher对象,监听expr的数据变化,一旦发生变化,执行回调函数
    new Watcher(vm, expr, (newValue, oldValue) => {
      node.textContent = newValue
    })
  },
  // 处理v-html指令
  html(node, vm, expr) {
    node.innerHTML = this.getVMValue(vm, expr) 
    new Watcher (vm, expr, newValue => {
      node.innerHTML = newValue
    })
  },
  // 处理v-model指令
  model(node, vm, expr) {
    let self = this
    node.value = this.getVMValue(vm, expr)
    // 实现双向数据绑定,给node注册input事件,当当前元素value值发生改变,修改对应的数据
    node.addEventListener('input', function () {
      self.setVMValue(vm, expr, this.value)
    })
    new Watcher(vm, expr, newValue => {
      node.value = newValue
    })
  },
  eventHandler(node, vm, type, expr) {
    // 给当前元素注册事件即可
    let eventType = type.split(":")[1]
    let fn = vm.$methods && vm.$methods[expr]
    if (eventType && fn) {
      node.addEventListener(eventType, fn.bind(vm))
    }
  },
  // 这个方法用于获取VM中的数据
  getVMValue(vm, expr) {
    // 获取到data中的数据
    let data = vm.$data
    expr.split(".").forEach(key => {
      data = data[key]
    })
    return data
  },
  setVMValue(vm, expr, value) {
    let data = vm.$data
    let arr = expr.split(".")

    arr.forEach((key, index) => {
      // 如果index是最后一个
      if(index < arr.length - 1) {
        data = data[key]
      } else {
        data[key] = value
      }
    })
  }
}

observe.js:

/**
 * observer用于给data中所有的数据添加getter和setter
 * 方便我们在获取或者设置data中数据的时候,实现我们的逻辑
 */
class Observer {
  constructor(data) {
    this.data = data
    this.walk(data)
  }
  /**
   * 核心方法
   * 遍历data中所有的数据,都添加上getter和setter
   */
  walk(data) {
    if (!data || typeof data != "object") {
      return
    }
    Object.keys(data).forEach(key => {
      // 给data对象的key设置getter和setter
      this.defineReactive(data, key, data[key])
      // 如果data[key]是一个复杂的类型,递归的walk
      this.walk(data[key])
    })
  }
  // 定义响应式的数据(数据劫持)
  // data中的每一个数据都应该维护有一个dep对象
  // dep保存了所有的订阅了改数据的订阅者
  defineReactive(obj, key, value) {
    let that = this
    let dep = new Dep()
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 如果Dep.target中有watcher对象,存储到订阅者数组中
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set(newValue) {
        if (value === newValue) {
          return
        }
        value = newValue
        // 如果newValue是一个对象,也应该对它进行劫持
        that.walk(newValue)
        // 发布通知, 让所有的订阅者更新内容
        dep.notify()
      }
    })
  }
}

watcher.js :

/**
 * watcher模块负责把compile模块与observe模块关联起来
 */
class Watcher {
  /**
 * vm: 当前vue实例
 * expr: data中数字的名字
 * 一旦数据发生改变,需要调用cb
 */
  constructor(vm, expr, cb) {
    this.vm = vm
    this.expr = expr
    this.cb = cb

    // this表示的就是新创建的watcher对象
    // 存储到Dep.target属性上
    Dep.target = this
    // 需要把expr的旧值给储存起来
    this.oldValue = this.getVMValue(vm, expr)
    // 清空Dep.target方便下一次使用
    Dep.target = null
  }

  // 对外暴露的一个方法,这个方法用于更新页面
  update () {
    let oldValue = this.oldValue
    let newValue = this.getVMValue(this.vm, this.expr)
    if (oldValue != newValue) {
      this.cb(newValue, oldValue)
    }
  }

  // 用于获取vm中的数据
  getVMValue(vm, expr) {
    // 获得到data中的数据
    let data = vm.$data
    expr.split(".").forEach(key => {
      data = data[key]
    })
    return data
  }
}

/**
 * dep对象用于管理所有的订阅者和通知这些订阅者
 */
class Dep{
  constructor () {
    // 用于管理订阅者
    this.subs = []
  }

  // 添加订阅者
  addSub(watcher) {
    this.subs.push(watcher)
  }

  // 通知
  notify () {
    // 遍历所有的订阅者, 调用watcher的update方法
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

你可能感兴趣的:(前端,VUE)