Vue的源码简单超详细分析MVVM原理

在这里跟大家分享下Vue的MVVM实现原理,主要是简单的实现了下数据的双向绑定,v-model和{{}}语法和计算属性。前面每句都有注释和步骤,但是后面的部分由于不好进行步骤划分,就只解释了原因。这部分呢都是看的前端大佬姜文老师的讲解实现的。如有不对的地方,请提出来。
目录
在这里插入图片描述
先看html文件

在这里插入代码片<!DOCTYPE html>
<html lang="en">
<head>
  <title>Vue的MVVM原理的简单实现</title>
</head>
<body>
  
  <div id="app">
    <input type="text" v-model="school.name">
    <div>{{school.name}}</div>
    <div>{{school.age}}</div>
    <div>{{getNewName}}</div>
    <ul>
      <li>1</li>
      <li>2</li>
    </ul>
  </div>
</body>
<script src="./yuanMa.js"></script>
  <script>
    let vm = new Vue({
      el: '#app',
      data: {
        school: {
          name: 'dapeng',
          age: 18,
        }
      },
      computed: {
        getNewName() {
          return this.school.name + '666'
        }
      }
    })
  </script>
</html>

接下来就是我们的重中之重,源码文件,实现上面的功能可以和Vue.js进行无缝对接。
yuanMa.js

class Dep { // 39.订阅,里面存放着观察者,如果数据一变,就通知对应的观察着进行更新
  constructor() {
    this.subs = []; // 存放所有的watcher
  }
  // 订阅
  addSub(watcher) { // 添加watcher
    this.subs.push(watcher)
  }
  // 发布
  notify() { // 这个方法会在Object.defineProperty中的set方法中调用,因为那个时候数据变化了,就改通知观察者更新视图了
    this.subs.forEach(watcher => watcher.update())
  }
}
class Watcher { // 37.观察者,拿到每个值的老值,然后当值变化了,我们就进行视图的更新,也就是重新改变input和textContent的值
  constructor(vm, expr, cb) {
    this.vm = vm
    this.expr = expr
    this.cb = cb
    // 37.默认存放个老值
    this.oldValue = this.get()
  }
  get() {
    Dep.target = this // 先把自己放在this上
    // 取值,把这个观察者和数据关联起来
    let value = CompileUtil.getVal(this.vm, this.expr)
    Dep.target = null // 
    return value
  }
  update() { // 38.数据更新后 ,会调用观察者的update方法
    let newVal = CompileUtil.getVal(this.vm, this.expr) // 更新的时候才会调用update,后面看
    if (newVal !== this.oldValue) { // 新旧老值比较,不一样就进行更新
      this.cb(newVal)
    }
  }
}
//30. 创建Observer类,数据劫持,劫持数据用Object.defineproperty来定义
//36. 但是这样的话,我们只是劫持了数据,但是数据变化的时候,视图还是不会更新的,因为我们还没处理,所以我们就需要观察者watcher和dep订阅
class Observer {
  constructor(data) {
    // console.log(data)
    this.observer(data) // 31.调用observer方法
  }
  observer(data) {
    if (data && typeof data == 'object') { // 判读数据合法性和是否是对象,是对象就用Object.defineproperty来定义
      for (let key in data) {
        this.defineReactive(data, key, data[key]) // 32. 将对象的属性进行转换,调用defineReactive
      }
    }
  }
  defineReactive(obj, key, value) { // 32.调用defineReactive
    this.observer(value) // 33.如果value也是对象的就会再次使用Object.defineproperty来定义
    let dep = new Dep() // 给每一个属性,都加上一个具有发布订阅的功能
    Object.defineProperty(obj, key, { // obj是 { school: { name: 'dapeng', age: 18 } }, key就是name或者age
      get() {
        Dep.target && dep.addSub(Dep.target) // 创建watcher时,会取到对应的内容,并且把watcher放到了全局上
        return value // 33. Object.defineproperty的get返回的值就是对应key的值
      },
      set: (newVal) => { // 这里使用箭头函数是因为下面的this指向问题
        if (newVal != value) {
          // 35.如果我们修改的新增是个对象,那么我们也要调用this.observer()
          this.observer(newVal)
          // 34.一旦修改对应key的值,Object.defineproperty的set的newVal就是修改后的那个值,我们就让原来值等于新值,get返回的就是修改后的值了
          value = newVal
          dep.notify() // 数据变化了就要通知视图进行更改
        }
      }
    })
  }
}

// 23. 在这里统一的地方来分别用不同的方法来处理v-开头的指令和{{}}
CompileUtil = {
  // 28.!!!! 我们这里做完了那就相当于编译完成了,接下来如果我们要做的就是数据的双向绑定,也就是使用Object.defineproperty()
  getVal(vm, expr) { // 25.这个方法是用来辅助取值的,expr是属性值school.name,从vue的实例上找出这个变量所代表的值,这里没有考虑数组
    return expr.split('.').reduce((data, current) => { // 最后执行完毕这个reduce方法,这里返回的就是dapeng
      return data[current];
    }, vm.$data)
  },
  setValue(vm, expr, value) { // 用来监听视图变化去改变数据的方法
    expr.split('.').reduce((data, current, index, arr) => { // school.name
      if (index == arr.length -1) { // 取到最后一层的时候,就代表可以赋值了
        return data[current] = value
      }
      return data[current];
    }, vm.$data)
  },
  model(node, expr, vm) { // 24.处理v-model的方法, node是节点,expr是属性值school.name,vm是Vue的实例
    // 给输入框赋予value属性 node.value = xxx
    let fn = this.updater['modelUpdater']
    new Watcher(vm, expr, (newVal) => { // 给输入框添加个观察者,如果稍后数据更新了会触发此方法会拿新值赋予输入框
      fn(node, newVal)
    })
    let value = this.getVal(vm, expr) // 25.expr是属性值school.name,从vue的实例上找出这个变量所代表的值
    fn(node, value) // 这里一执行,那么input输入框的值就变为school.name所代表的值了
    node.addEventListener('input', (e) => { // 解析v-model的指令,监听input输入框,当值改变后,我们拿到新增去改变vm.$data对应变量的值
      let value = e.target.value
      this.setValue(vm, expr, value)
    })
  },
  html() { // 24.处理v-html的方法 node.innerHTML = xxx

  },
  getContentValue(vm, expr) {
    // 遍历表达式,将内容重新替换成一个完整的内容,返还回去
    return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
      return this.getVal(vm, args[1])
    })
  },
  text(node, expr, vm) { // 24.处理{{}}语法的方法
    let fn = this.updater['textUpdater']
    let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
      // console.log(args[1])
      new Watcher(vm, args[1], () => {
        fn(node, this.getContentValue(vm, expr)) // 返回了一个新的字符串
      })
      return this.getVal(vm, args[1])
    })
    fn(node, content)
  },
  updater: {
    modelUpdater(node, value) { // 26.处理v-model的方法,给输入框赋予value属性 node.value = xxx
      node.value = value
    },
    htmlUpdater() {

    },
    textUpdater(node, value) {// 27.处理{{}}语法
      node.textContent = value // 这样能让对应节点的内容为编译后的内容
    }
  }
}

// 4.创建一个专门进行编译替换的类,将数据和页面的指令等等编译替换
class Compiler {
  constructor(el, vm) { //5.这里的el可能就是个字符串如#app,也有可能是元素document.getElementById('app'),所以我们要进行判断
  this.el = this.isElementNode(el) ? el : document.querySelector(el)
  this.vm = vm
  // console.log(this.el)
  // 7. 然后我们就可以进行替换,但是这样有个问题,我们如果直接替换的话,那就是挨个去找,找到个语法替换个语法,这样页面就一直刷新,这样不行,
  // 所以我们可以把内容全部放在内存中去,当全部替换之后在放到页面上
  // 我们可以把当前的元素节点,如这里的#app下所有的内容放到内存中去,我们就定义个存放到内存中的方法
  let fragment = this.node2fragment(this.el)
  // 13. 然后我们就可以把节点中的内容进行替换,编辑模板,用数据编译,然后把内容塞在页面中,这就是我们接来下需要做的,慢慢来
  // 14. 进行编译模板,用我们创建vue实例时传入的数据去编译,this.compile(fragment)
  this.compile(fragment)
  // 把内容在塞到页面中,注意: 这一步是在上面所有模板编译完成之后进行的
  this.el.appendChild(fragment)

  }
  // 22.判断这个属性是不是v-开头的方法
  isDirective(attrName) {
    return attrName.startsWith('v-') // 该方法判断一个字符串是否在另一个字符串的头部,返回一个布尔值,true表示存在,es6的新东西
  }
  // 18. 创建编译节点和文本的方法compileElement(child)和compileText(child)
  // 19. 编译元素的方法
  compileElement(node) {
    let attributes = node.attributes; // 20.拿到元素的属性,看上面有没有v-开头的指令
    // console.log(node, attributes);
    [...attributes].forEach(attr => {
      let { name, value } = attr
      if (this.isDirective(name)) { // 21.判断这个属性是不是v-开头的指令this.isDirective()
        // console.log(name)
        let [, directive] = name.split('-') // directive这里就是model
        // 22. 我们在这里找到v-开头的属性和19步的compileText方法找到了{{}}语法的内容,那么我们就需要
        // 在一个统一的地方来分别用不同的方法来处理,CompileUtil
        CompileUtil[directive](node, value, this.vm) // 调用这个方法,来处理不同的指令, value这里就是school.name,这this.vm上的data有我们school.name代表的值
      }
    })
  }
  // 19. 编译文本的方法
  compileText(node) {
    let content = node.textContent // node.textContent可以拿到当前节点的文本内容
    // console.log(content) 
    if (/\{\{(.+?)\}\}/.test(content)) { // 判断文本节点中内容是否包含{{ xxx }} {{ aaa }}
      // console.log(content)
      CompileUtil['text'](node, content, this.vm)
    }
  }
  //15. 创建用来编译内容中的dom节点的方法
  compile(node) {
    let childNodes = node.childNodes; // 16.这里拿到的是第一层的节点,就是第一层元素和空白节点,不包括子节点下的孙子节点
    // console.log(childNodes) // [text, input, text, div, text, div, text, ul, text]
    // 17. 判断节点是不是元素节点
    [...childNodes].forEach(child => {
      if (this.isElementNode(child)) {
        // console.log('element', child)
        this.compileElement(child)
        // 如果是元素的话,就需要把自己传进去,再去遍历自己的子节点
        this.compile(child)
      } else {
        // console.log('text', child) // 这里要处理空白节点是因为我们可能直接在#app下写个{{ school.name}}
        this.compileText(child)
      }
    })
  }
  // 8.创建个方法把app下的所有内容都放在内存中去
  node2fragment(node) {
    let fragment = document.createDocumentFragment(); // 9.这是创建一个文档碎片
    let firstChild
    while(firstChild = node.firstChild) {
      // 10.fragment有个方法appendChild,具有移动性,能把东西放在内存中去,放一个到内存中,那么页面就少一个
      fragment.appendChild(firstChild) // 11. 每放一个firstChild,那么页面上就少个节点,那么下个节点就又变成firstChild,这样循环下去,就
      //把app下的所有节点全部放在页面中去了,那么此刻页面上就没有内容了。
    }
    // 12.当所有节点都被放在内存中之后,循环终止,那么就把内容中的fragment文档碎片返回出去
    // console.log(fragment)
    return fragment
  }
  // 6. 判断是不是元素节点
  isElementNode(node) {
    return node.nodeType === 1; // 如果一个节点的nodeType为1,那么就是DOM元素
  }
}
//1.创建一个基类,Vue
class Vue {
  constructor(options) {
    this.$el = options.el // 2.将创建的vue实例的时候传入的数据传入到options中
    this.$data = options.data
    let computed = options.computed // 先不管计算属性,建议看完了编译渲染和数据劫持在回看computed
    //3.如果传入了el,则我们需要进行将内容进行编译,将数据和页面的东西编译在一起,所以创建一个专门进行编译的类
    if (this.$el) {
      new Observer(this.$data) // 29.回到这里,我们要把this.$data的数据全部转换成Object.defineProperty来定义,我们定义一个Observer类
      // console.log(this.$data)
      for (let key in computed) { // 先不管计算属性,建议看完了编译渲染和数据劫持在回看computed
        Object.defineProperty(this.$data, key, { // 给this.$data上添加了个属性,这里为getNewName
          get: () => {
            return computed[key].call(this)
          }
        })
      }
      this.proxyVm(this.$data) // 这个地方可以后面在看,只是做代理,让vm.school就相当于vm.$data.school
      new Compiler(this.$el, this) // 3.创建一个专门进行编译的类
    }
  }
  proxyVm(data) {
    for (let key in data) { // { school: {name, age } }
      Object.defineProperty(this, key, { // this就是实例vm,这里如key就是school,让vm多了个属性是school,值就是$data.school的值
        get() {
          return data[key]
        }
      })
    }
  }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值