Vue React源码看这真香系列之MVVM欢迎转载

201 阅读2分钟

vue.js是一个mvvm的渐进式js框架。从字面就可以理解为视图关联数据数据关联视图。所以我们要做到的就是把数据关联到视图中,操作数据会重新渲染视图,操作视图会改变数据等。

1.使用中可以知道Vue本身是一个构造函数所以第一步我需要一个构造函数。

class Vue {
  constructor(opt) {
    this.$el = opt.el;
    this.$data = opt.data;
    this.computed = opt.computed;
    // 专门编译模板的
    if (this.$el) {
      console.log(this.computed)
      for(let key in  this.computed){
        console.log(key)
        Object.defineProperty(this.$data,key,{
            get:()=>{
                return this.computed[key].call(this)
            }
        });
      }
      // 数据劫持
      new Obsever(this.$data)
      this.proxyVM(this.$data)
      // 调用编译器
      new Complier(this.$el, this)
    }
  }
  proxyVM(data){
    for(let key in data){
      Object.defineProperty(this,key,{
        get(){
          return data[key]
        }
      })
    }
  }
}

2.编译器负责把数据挂载在DOM中

// 编译器(把数据挂在DOM种)
class Complier {
  constructor(el, vm) {
    // 是元素就为元素否则就获取
    this.el = this.isElementNode(el) ? el : document.querySelector(el)
    this.vm = vm

    // 调用文档碎片,把DOM都放到内存中提高性能,文档碎片就相当于一个塑料袋,只操作一次DOM
    let frag = this.fragmentNode(this.el)

    // 处理文档碎片中的属性
    this.complier(frag)

    // 把文档碎片放到el中
    this.el.append(frag)
  }
  // 判断是否为元素节点
  isElementNode(node) {
    // 判断节点类型是否为元素节点
    return node.nodeType === 1
  }
  // 文档碎片
  fragmentNode(node) {
    // 创建文档碎片
    let frag = document.createDocumentFragment()
    // DOM操作都是剪切,所以要一直哪到第一个子节点,赋值给变量
    let firstChild;
    // 循环拿到子节点
    while (firstChild = node.firstChild) {
      // 把每个子节点放到文档碎片种
      frag.append(firstChild)
    }
    // 把生成好的文档碎片return 出去
    return frag
  }
  // 处理文档碎片中的属性
  complier(frag) {
    // console.log(frag.childNodes)
    // 因为文档碎片中的所有的子节点组成的是一个类数组,所以要转化成一个数组才方便进行操作
    let nodes = [...frag.childNodes]
    // console.log(nodes)
    // 循环所有的子节点
    nodes.forEach(node => {
      // 判断是否为元素节点
      if (this.isElementNode(node)) {
        // 获取所有属性
        let attrs = [...node.attributes]
        console.log(attrs)
        // 循环属性
        attrs.forEach(attr => {
          // 看看有没有v-的属性
          // attr.nodeName.startWith('v-')
          if(/^v-/.test(attr.nodeName)){
            // 拿到属性值
            console.dir(attr)
            let {nodeValue} = attr
            // 当对数据进行赋值该值操作时就需要这个watcher
            new Watcher(this.vm,nodeValue,(newVal) => {
              node.value = newVal
            })

            // 在数据里拿到数据
            let value = this.vm.$data[nodeValue]


            // 当改变值的时候数据也变
            node.oninput = (ev) => {
              this.vm.$data[nodeValue] = ev.target.value
              console.log(this.vm.$data.num)
            }
          
            // 赋值
            node.value = value;
          }
        })
      }else{ // 当不是元素节点时也就是文本节点
        // 判断文本节点是否有{{}},如果有就同步数据
        console.dir(node)
        // /\{\{(\w+)\}\}/.test(node.nodeValue)
        if(/\{\{(\w+)\}\}/.test(node.nodeValue)){
          // 拿到文本
          let str = node.nodeValue,key;
          // console.log(str)
          // 替换数据
          let nodeVal = str.replace(/\{\{(\w+)\}\}/g,(...arg) => {
            key = arg[1]
            console.log(key)
            // console.log(this.vm.$data)
            return this.vm.$data[arg[1]]
          })
          console.log(nodeVal)
          // console.log(key)
          new Watcher(this.vm,key,(newVal) => {
            node.nodeValue = newVal  //把最新的数据赋值给有小胡子的{{}}文本
          })

          // 给新的数据赋值
          // console.dir(nodeVal)
          // console.log(node)
          node.nodeValue = nodeVal
        }
      }

    })
  }
}

3.发布订阅器

class Dep{
  constructor(){
    this.sub = []
  }
  addSub(watcher){
    this.sub.push(watcher)
    console.log(this.sub)
  }
  notify(){
    this.sub.forEach(watcher => {
      watcher.updata()
    })
  }
}

4.观察者模式,通过监控当前值的变化来关联视图,当新值和老值不一样的时候执行回调函数

class Watcher{
  // vm -> vm.$data key -> 监控的数据 cb -> 当数据变化的时候执行的回调
  constructor(vm,key,cb){
    // 当实例化Watcher的时候把实例挂到Dep的属性下方便所有人拿到
    Dep.target = this;
    this.vm = vm
    this.key = key
    this.cb = cb
    // 一上来就获取一次作为老的值
    this.oldVal = this.get()
    //把实例用完之后把Dep.target给置空,防止只要是get数据就push Watcher
    Dep.target = null;
  }
  get(){
    // 通过参数获取老值
    let val = this.vm.$data[this.key]
    return val
  }
  // 数据更新的时候进行对比,当set的时候才拿得到新值
  updata(){
    let newVal = this.get()
    if(this.oldVal !== newVal){
      // 当数据改变时调用回调函数
      this.cb(newVal)
    }
  }
}

数据劫持

class Obsever{
  constructor(data){
    // 调用循环
    this.obsever(data)
  }
  obsever(data){
    // 判断有数据并且为对象就循环对象,并且进行数据劫持
    if(data && Object.prototype.toString.call(data) === '[object Object]'){
      for(let key in data){
        this.defineReactive(data,key,data[key])
      }
    }
    console.log(data) // 每个数据都有数据数据劫持
  }
  // 数据劫持
  defineReactive(obj,key,val){
    // 进行深度劫持,
    if(typeof val === 'object'){
      this.obsever(val)
    }
    let dep = new Dep
    Object.defineProperty(obj,key,{
      get(){
        
        // 在读取数据的时候进行订阅
        // console.log(Dep.target,'1111')
        Dep.target && dep.addSub(Dep.target)
        // console.log(Dep.target,'1111')
        return val
      },
      // 箭头函数this是上下文作用域
      set:(newVal) => {
        if(val !== newVal){
          // 当最新的val是引用数据类型,保证新值的数据也被数据劫持了
          this.obsever(newVal)
          val = newVal
          // 在
          dep.notify()
        }
      }
    })
  }
}