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


    19.6.28更新:

    这篇博客比较完善:将每一部分都分装在单独的js文件中:

    剖析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)
      }
    }
  • 相关阅读:
    ASP.NET——From验证:全部代码及讲解
    JavaScript 经典代码大全:有 目录 及 编号 的哦 !
    很好的一首英文歌曲:不论是旋律、还是歌词或者MV
    2007年10月份_很想念大家
    NND,8月没有来发贴,现在是9月了,要发一个
    买了一个新的域名和主机,呵呵,
    视频下载:HTML基础及应用
    简单的哲理,放在最上面,提醒自己
    学的东西忘记得差不多啦......
    欲找情人 要做哪些准备?
  • 原文地址:https://www.cnblogs.com/houfee/p/10938914.html
Copyright © 2020-2023  润新知