开课吧直播课(1)如何通过数据劫持实现Vue(mvvm)框架

半个月前看的直播课,现在才自己敲了一遍,罪过罪过

预览:

思路:

简单实现Vuemvvm的双向数据绑定,需要以下几个步骤:

  1. 实现一个入口,把 指令渲染,数据劫持

  2. 实现指令渲染,包括层级嵌套的标签,文本

  3. 数据劫持

  4. 订阅发布

1.实现一个入口文件

  let vm = new Kvue({
    el: "#app",
    data: {
      message: "测试数据",
      options: "123",
      name: "张三"
    }
  })

2.替换{{}}中的数据

class Kvue {
  constructor(options) {
    // 将传入的数据挂载到 Kvue 上
    this.$options = options
    this._data = options.data

    // 编译 {{}},此时需要把编译的范围当做入参
    this.compile(options.el)
  }

  // 模板替换
  compile(el) {
    // 获取挂载点
    let element = document.querySelector(el)
    this.compileNode(element)
  }

  // 递归节点
  compileNode(element) {
    // 获取 childNodes
    let childNodes = element.childNodes
    // 将 childNodes 转换为 真正的数组
    Array.from(childNodes).forEach(node => {
      // 文本节点 nodeType = 3
      if(node.nodeType == 3) {
        // console.log(node)
        // 获取节点内容
        let nodeContent = node.textContent
        // 使用正则匹配{{}},去除其中的空格
        let reg = /\{\{\s*(\S*)\s*\}\}/
        if(reg.test(nodeContent)) {
          // console.log(RegExp.$1)
          node.textContent = this._data[RegExp.$1]
        }
      } else if (node.nodeType == 1) {
        // 标签节点
        let attrs = node.attributes
        // console.log(attrs)
        // 遍历标签节点
        Array.from(attrs).forEach(attr => {
          // 获取标签的属性
          let attrName = attr.name
          // 获取标签的值
          let attrValue = attr.value
          // console.log(attrValue)
          // 匹配是否是 k- 开头的指令
          if(attrName.indexOf('k-') == 0) {
            // 获取 k- 后面的部分,
            attrName = attrName.substr(2)
            // console.log(attrName)
            // 目的是防止用户自定义 k-holle 的属性
            if(attrName == "model") {
              // 将 data 中的对应值赋给此节点
              node.value = this._data[attrValue]
            }
            // 监听 input 变化
            node.addEventListener('input', e => {
              console.log(e.target.value)
              this._data[attrValue] = e.target.value
            })
          }
        })
      }
      // 递归判断是否有子节点
      if(node.childNodes.length > 0) {
        this.compileNode(node)
      }
    })
  }
}

3.数据劫持

认识 defineProperty()

  // let obj = {name: "张三"}
  // console.log(obj);
  // obj.name = "李四"

  // 数据劫持
  let obj = Object.defineProperty({}, "name", {
    configurable: true, // 可配置
    enumerable: true, // 枚举
    get() {
      console.log("get");
      return "张三" // 必须 return
    },
    set(newValue) {
      console.log("set", newValue);
    }
  })
  console.log(obj);

实现数据劫持

  // 数据劫持
  observer(data) {
    Object.keys(data).forEach(key => {
      let value = data[key]
      Object.defineProperty(data, key, {
        configurable: true,
        enumrable: true,
        get() {
          return value
        },
        set(newValue) {
          // console.log("set", newValue)
          value = newValue
        }
      })
    })
  }

现在实现了数据劫持,那么数据变化,就需要通知 observer 去更新视图,这时就需要一个订阅发布模式

4.订阅发布,视图更新

订阅发布模式:

demo:

老王给孩子或者邻居通过电话讲故事,但是有时候电话没人接,老王需要重新打一次。这时就想到了发布订阅模式:老王将讲的故事录成视频,存到网上,然后孩子和邻居注册报备一下,老王知道谁订阅了他的故事,然后老王群发一个消息,让他们自己去看

// 发布订阅模式
// 老王,订阅收集器
class Dep {
  constructor() {
    // 把 孩子 邻居 放在一个容器中存起来
    this.subs = []
  }

  // 注册报备
  addSub(sub) {
    this.subs.push(sub)
  }

  // 发布视频,通知 孩子 邻居 更新
  notify() {
    this.subs.forEach(v => {
      v.update();
    })
  }
}

// 订阅者 孩子,邻居
class Watcher {
  constructor() {

  }
  // 
  update() {
    console.log('更新了');
  }
}

// 实力化 老王
let dep = new Dep()

// 孩子 邻居
let watcher1 = new Watcher()
let watcher2 = new Watcher()
let watcher3 = new Watcher()

// 孩子 邻居 注册报备
dep.addSub(watcher1)
dep.addSub(watcher2)
dep.addSub(watcher3)

// 发布视频
dep.notify()

MVVM实现订阅发布

在数据劫持结合订阅发布模式实现视图更新(难点)

// 发布订阅模式
class Dep {
  constructor() {
    this.subs = []
  }

  addSub(sub) {
    this.subs.push(sub)
  }

  notify(newValue) {
    this.subs.forEach(v => {
      // console.log(newValue)
      v.update(newValue);
    })
  }
}

class Watcher {
  constructor(vm, exp, cb) {
    // 在更新时,实例化,在什么位置加呢?在调取数据时添加Watcher,但是在加的时候先声明处订阅收集器——老王 —— get() {}
    // 防止重复添加
    Dep.target = this
    // 触发 get 方法
    vm._data[exp]
    // 改变视图的回调
    this.cb = cb
    // 防止重复添加
    Dep.target = null
  }
  update(newValue) {
    console.log('更新了', newValue)
    // 改变视图
    this.cb(newValue)
  }
}

总结

简单实现vue的双向绑定,没有涉及复杂的对象

代码冗余,没有抽离

Kvue 类太复杂,没有把 数据劫持,订阅发布,代码编译 抽离成单独的 js 文件

未完待续。。。

全部代码

index.html

<head>
    <meta charset="UTF-8">
    <title>如何通过数据劫持实现Vue(mvvm)框架</title>
    <script src="./kvue.js"></script>
</head>

<body>
  <div id="app">
    {{message}}
    <p>{{message}}</p>
    <hr>
    <input type="text" k-model="name">
    {{name}}
  </div>
  <script>
    let vm = new Kvue({
        el: '#app',
        data: {
            message: '测试数据',
            name: '张三'
        }
    })
    // 模拟数据改变,实现视图更新 
    setTimeout(() => {
      vm._data.message = "修改的值"
    }, 2000)
    // vm._data.message = "修改的值"
    // vm._data.name = "ls"
    // vm.message
    // vm.options
  </script>
</body>

kvue.js

class Kvue {
  constructor(options) {
      // 将传入的数据挂载到 Kvue 上
      this.$options = options
      this._data = options.data

    // 劫持数据 defineProperty()
    this.observer(this._data)

      // 编译 {{}},此时需要把编译的范围当做入参
      this.compile(options.el)
  }

  // 数据劫持
  observer(data) {
    Object.keys(data).forEach(key => {
      let value = data[key]
      // 订阅收集器
      let dep = new Dep()
      // 数据劫持
      Object.defineProperty(data, key, {
        configurable: true, // 可配置
        enumrable: true, // 枚举
        // get 需要触发
        get() {
          // 如果 Dep 中有 target,添加addSub()
          if(Dep.target) {
            dep.addSub(Dep.target)
          }
          return value // 必须 return
        },
        set(newValue) {
          // console.log("set", newValue)
          if(newValue !== value)
          value = newValue
          // 当改变时 通知 update(),更新UI视图
          dep.notify(newValue)
        }
      })
    })
  }

  // 模板替换
  compile(el) {
      // 获取挂载点
      let element = document.querySelector(el)
      this.compileNode(element)
  }

  // 递归节点
  compileNode(element) {
    // 获取 childNodes
    let childNodes = element.childNodes
    // 将 childNodes 转换为 真正的数组
    Array.from(childNodes).forEach(node => {
      // 文本节点 nodeType = 3
      if(node.nodeType == 3) {
        // console.log(node)
        // 获取节点内容
        let nodeContent = node.textContent
        // 使用正则匹配{{}},去除其中的空格
        let reg = /\{\{\s*(\S*)\s*\}\}/
        if(reg.test(nodeContent)) {
          // console.log(RegExp.$1)
          node.textContent = this._data[RegExp.$1]
          // 初次渲染 实例化 Watcher,并且防止递归过程中重复添加
          // 将 this 传进来,目的是传 this 下的 data, 还有 下标 cb 是回调,作用是更新视图,不建议在 订阅发布中更新视图
          new Watcher(this, RegExp.$1, newValue => {
            // 更新视图
            // console.log(newValue)
            node.textContent = newValue
          })
        }
      } else if (node.nodeType == 1) {
        // 标签节点
        let attrs = node.attributes
        // console.log(attrs)
        // 遍历标签节点
        Array.from(attrs).forEach(attr => {
          // 获取标签的属性
          let attrName = attr.name
          // 获取标签的值
          let attrValue = attr.value
          // console.log(attrValue)
          // 匹配是否是 k- 开头的指令
          if(attrName.indexOf('k-') == 0) {
            // 获取 k- 后面的部分,
            attrName = attrName.substr(2)
            // console.log(attrName)
            // 目的是防止用户自定义 k-holle 的属性
            if(attrName == "model") {
              // 将 data 中的对应值赋给此节点
              node.value = this._data[attrValue]
            }
            // 监听 input 变化
            node.addEventListener('input', e => {
              this._data[attrValue] = e.target.value
            })
            // 注册
            new Watcher(this, attrValue, newValue => {
              node.value = newValue
            })
          }
        })
      }
      // 递归判断是否有子节点
      if(node.childNodes.length > 0) {
        this.compileNode(node)
      }
    })
  }
}

// 发布订阅模式
class Dep {
  constructor() {
    this.subs = []
  }

  addSub(sub) {
    this.subs.push(sub)
  }

  notify(newValue) {
    this.subs.forEach(v => {
      // console.log(newValue)
      v.update(newValue);
    })
  }
}

class Watcher {
  constructor(vm, exp, cb) {
    // 在更新时,实例化,在什么位置加呢?在调取数据时添加Watcher,但是在加的时候先声明处订阅收集器——老王 —— get() {}
    // 防止重复添加
    Dep.target = this
    // 触发 get 方法
    vm._data[exp]
    // 改变视图的回调
    this.cb = cb
    // 防止重复添加
    Dep.target = null
  }
  update(newValue) {
    console.log('更新了', newValue)
    // 改变视图
    this.cb(newValue)
  }
}

猜你喜欢

转载自www.cnblogs.com/houfee/p/10938914.html