MVVM原理- 2 -简单版vue实现

<!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">
        <p>{{ name }}</p>
        <div v-text="name"></div>
        <input  v-model="name" type="text">

    </div>
    <script>
        // 手写一个mvvm 简易版的vuejs
        // options就是选项 全部vue属性都带$
        function Vue (options) {
            this.subs = {} // 事件管理器
            this.$options = options  // 放置选项
           this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
           // 将dom对象赋值给$el 和官方vuejs保持一致
         this.$data = options.data || {}
         //  数据代理 但愿 vm可以代理 $data的数据 
         // 但愿 vm.name 就是$data.name
         this.$proxyData() // 代理数据  把$data中数据 代理给vm实例
         this.$observer()  // 数据劫持  劫持 $data中的数据变化
         this.$compile(this.$el) // 模板第一次编译渲染 递归的要求 这里必须传入参数 
         // 递归是一种简单的算法 => 通常用在处理树形数据,嵌套数据 中国/北京/海淀/中关村/知春路/海淀桥/982/人
         // 递归其实就是函数自身调用自身 => 传入下一次递归的条件 => 两次递归条件同样 => 死循环了
        }
        // 数据代理好的方法
        Vue.prototype.$proxyData = function () {
            // this 指的就是 当前的实例
            // key 就是 data数据中的每个key
            Object.keys(this.$data).forEach(key => {
                // 代理数据 让vm实例代理$data中的数据
                Object.defineProperty(this, key, {
                    // 存取描述符
                    get () {
                       return this.$data[key]  // 返回$data中的数据
                    },
                    // 设置数据时 须要 将 值设置给 $data的值 并且要判断设置以前数据是否相等
                    set (value) {
                        // value是新值 若是新值等于旧值 就不必再设置了
                        if (this.$data[key] === value ) return
                        this.$data[key] = value // 若是不等再设置值
                    }
                })
            })
            
        }
        // 数据劫持
        Vue.prototype.$observer = function () {
            // 要劫持谁 ? $data
            // 遍历 $data中的全部key
            Object.keys(this.$data).forEach(key => {
               // 劫持 =>劫持数据的变化 -> 监听 data中的数据的变化 => set方法
               // obj / prop / desciptor
               let value = this.$data[key] // 从新开辟一个空间  value的空间
               Object.defineProperty(this.$data, key, {
                   // 描述 => 描述符有几种 ? 数据描述符(value,writable) 存取描述符 (get/set)
                   get () {
                       return value
                   },
                   set: (newValue) => {
                      if(newValue === value) return 
                      value = newValue
                    //   一旦进入set方法 表示 MVVM中的 M 发生了变化  data变化了
                    // MVVVM => Model =>  发布订阅模式  => 更新Dom视图
                      // 总体编译只执行一次 经过发布订阅模式来作 触发一个事件 视图层监听一个事件
                     // 触发一个事件
                     this.$emit(key) // 把属性当成事件名 触发一个事件
                   }
               }) 
            })
        }
    
    // 编译模板 数据发生变化  => 模板数据更新到最新
        
       // 编译模板的一个总方法 构造函数执行时执行
        // rootnode是传入本次循环的根节点 => 找rootnode下全部的子节点 => 子节点 => 子节点=> 子节点 > 子节点 ...  找到没有子节点为止
       Vue.prototype.$compile = function (rootnode) {
         let nodes = Array.from(rootnode.childNodes)  // 是一个伪数组 将伪数组转成真数组
         nodes.forEach(node => {
            //  循环每一个节点 判断节点类型 若是你是文本节点 就要用文本节点的处理方式 若是元素节点就要元素节点的处理方式
            if(this.$isTextNode(node)) {
                // 若是是文本节点
                this.$compileTextNode(node) // 处理文本节点 当前的node再也不有 子节点 没有必要继续找了
            }
            if(this.$isElementNode(node)) {
                // 若是是元素节点
                this.$compileElementNode(node) // 处理元素节点
                // 若是是元素节点 下面必定还有子节点 只有文本节点才是终点
                // 递归了 => 自身调用自身
                this.$compile(node) // 传参数 保证一层一层找下去 找到 node.chidNodes的长度为0的时候 自动中止
                // 能够保证 把 $el下的全部节点都遍历一遍
            } 
         })
       }
       // 处理文本节点 nodeType =3
       Vue.prototype.$compileTextNode = function (node) {
            // console.log(node.textContent)
            // 拿到文本节点内容以后 要作什么事情 {{ name }}  => 真实的值 
            // 正则表达式 
            const text = node.textContent // 拿到文本节点的内容 要看一看 有没有插值表达式
             const reg = /\{\{(.+?)\}\}/g  // 将匹配全部的 {{ 未知内容 }}
            if (reg.test(text)) {
                // 若是能匹配 说明 此时这个文本里有插值表达式
                 // 表示 上一个匹配的正则表达式的值
                const key = RegExp.$1.trim() // name属性 => 取name的值 $1取的是第一个的key
                 node.textContent = text.replace(reg,  this[key] ) 
                  // 获取属性的值 而且替换 文本节点中的插值表达式
                this.$on(key, () => {
                    // 若是 key这个属性所表明的值发生了变化 回调函数里更新视图
                    node.textContent = text.replace(reg, this[key] )    // 把原来的带大括号的内容替换成最新值 赋值给textContent
                })
            }
       }
    
       // 处理元素节点 nodeType = 1的时候是元素节点
       Vue.prototype.$compileElementNode = function (node) {
           // 指令 v-text  v-model  => 数据变化  => 视图更新 更新数据变化 
           // v-text = '值' => innerText上  textContent
           // 拿到该node全部的属性 
          let attrs = Array.from(node.attributes) // 把全部的属性转化成数组
        // 循环每一个属性  属性是否带 v- 若是带 v- 表示指令
            attrs.forEach(attr => {
               if (this.$isDirective( attr.name)) {
                //   判断指令类型
                    if(attr.name === 'v-text') {
                        // v-text的指令的含义是 v-text后面的表达的值 做用在 元素的innerText或者textContent上
                      node.textContent = this[attr.value]   // 赋值 attr.value => v-text="name"
                      this.$on(attr.value, () => {
                        node.textContent = this[attr.value]   //此时数据已经更新
                      })
                    }
                    if(attr.name === 'v-model') {
                        // 表示我要对当前节点进行双向绑定
                      node.value =  this[attr.value]   // v-model要给value赋值 并非textContent
                      this.$on(attr.value, () => {
                        node.value = this[attr.value]   //此时数据已经更新
                      })
                      node.oninput = () => {
                        //   须要把当前最新的节点的值 赋值给 自己的数据
                        this[attr.value] =  node.value  // 视图 发生 => 数据发生变化
                      }  // 若是一个元素绑定了v-model指令 应该监听这个元素的值改变事件
                    }

               } // 若是以 v-开头表示 就是指令
            })
       }
    
      //  判断一个节点是不是文本节点 nodeType ===3
       Vue.prototype.$isTextNode = function (node) {
          return node.nodeType === 3  // 表示就是文本节点
       }
      // 判断 一个节点是不是元素节点

       Vue.prototype.$isElementNode = function (node) {
        return node.nodeType === 1  // 表示就是元素节点
       }
       
      // 判断一个属性是不是指令 全部的指令都以 v-为开头 
      Vue.prototype.$isDirective = function (attrname) {
         return attrname.startsWith('v-')
      }
    
      // Vue的发布订阅管理器 $on $emit 
      //  监听事件
      Vue.prototype.$on = function (eventName, fn) {
                  //   事件名 => 回调函数  => 触发某个事件的时候 找到这个事件对应的回调函数 而且执行
        //  if(this.subs[eventName]) {
        //      this.subs[eventName].push(fn)
        //  }else {
        //      this.subs[eventName] = [fn]
        //  }
        this.subs[eventName] = this.subs[eventName] || []
        this.subs[eventName].push(fn)
      }
      // 触发事件
      Vue.prototype.$emit = function (eventName, ...params) {
       //  拿到了事件名 应该去咱们的开辟的空间里面 找有没有回调函数
       if(this.subs[eventName]) {
            //   有人监听你的事件
            // 调用别人的回调函数
            this.subs[eventName].forEach(fn => {
                // 改变this指向
               //  fn(...params) // 调用该回调函数 而且传递参数
                 // 三种方式 改变回调函数里的this指向
               //   fn.apply(this, [...params]) // apply 参数 [参数列表]
              //  fn.call(this, ...params) // 若干参数
               fn.bind(this, ...params)() // bind用法 bind并不会执行函数 而是直接将函数this改变
            });
          }
      }

        var vm =  new Vue({
            el: '#app', // 还有多是其余选择器 还有多是dom对象
            data: {
                name: '吕布',
                wife: '貂蝉'
            }
        })
    </script>
</body>
</html>
相关文章
相关标签/搜索