欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

300行代码实现Vue的MVVM响应式原理

程序员文章站 2022-03-30 10:45:38
...

前言

源码下载

Vue的响应式原理是面试老生常谈的问题了,而大多数人会选择直接背答案这样的形式去应付面试,一旦面试官继续追问,便什么也答不上来了,所以,我希望能通过参考Vue源码的形式,动手编写代码(大概300行左右)实现一个简单的Vue的MVVM框架,从而让我们更好的理解Vue的响应式原理。

我们会通过

  • 实现指令解析器Compile
  • 实现数据监听器Observer
  • 实现观察者Watcher

来实现整个Vue的MVVM的响应式原理

实现的功能不是很完善,如果大家有兴趣,可以自己补充,本次代码主要是完成整个响应式原理的流程,理解Observer,Compile,Watcher是什么,用来干什么,如何通过这三者,搭建起整个MVVM架构的桥梁,从而实现Vue的响应式原理。

300行代码实现Vue的MVVM响应式原理

创建一个简单的html作为测试

<div id="app">
  <div>{{person.name}} -- {{person.age}}</div>
  <div>{{person.fav}}</div>
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
  </ul>
  <div v-html="htmlStr" v-bind:style="{backgroundColor:red}"></div>
  <div v-text="person.fav"></div>
  <!-- <input type="text" v-bind:value='msg'> -->
  <input type="text" v-model='msg'>
  <div>{{msg}}</div>
  <h2 v-text="tip" v-on:click="clickMe"></h2>
</div>

300行代码实现Vue的MVVM响应式原理

和Vue一样在下面的script标签中创建一个Vue对象

// 为了避免跟Vue冲突 这里使用MVue作为类名
const app = new MVue({
    el: "#app",
    data: {
      person: {
        name: 'GHkmmm',
        age: '21',
        fav: '编程'
      },
      msg: '理解Vue的双向绑定原理',
      tip: '点我',
      htmlStr: 'hello world!'
    },
    methods: {
      clickMe(){
        console.log('click');
      }
    }
  })

创建MVue类

用于接收传入的参数

class MVue{
  constructor(options){
    this.$options = options;
    this.$el = options.el;
    this.$data = options.data;

    if(this.$el){
      // 实现一个指令解析器
      new Compile(this.$el, this);
    }
  }
}

实现一个指令解析器Compile

class Compile{
  constructor(el, vm){
    this.el = this.isElementNode(el) ? el : document.querySelector(el);
    this.vm = vm;
    /*
    	对页面内容进行编译的时候,因为需要遍历每个对象,所以不可能一一去更改
    	而是通过文档碎片的形式,统一修改完,再使用fragment去更新整个页面
    */
    // 1.获取文档碎片对象,放入内存中,减少页面的回流与重绘
    const fragment = this.nodeFragment(this.el);
    // 2.编译模版
    this.compile(fragment);
    // 3.追加子元素到根元素
    this.el.append(fragment);
  }
}

获取文档碎片

遍历子节点,添加到文档碎片对象中

nodeFragment(node){
  // 创建文档碎片对象
  const f = document.createDocumentFragment();
  let firstChild;
  //遍历<div id="app">下的子节点
  while(node.firstChild){
    firstChild = node.firstChild;
    // 将遍历得到的节点添加到文档碎片对象中
    f.append(firstChild);
  }
  // 返回文档碎片对象
  return f;
 }

编译模版

传入fragment之后,我们需要对fragment中的节点进行遍历,判断是元素节点还是文本节点,因为两者的处理方式不同,所以需要使用不同的方法进行编译

compile(fragment){
  	// 传入刚刚得到的文档碎片对象fragment
    // 1.获取子节点
    const childNodes = fragment.childNodes;
  	// 遍历childNodes数组
    [...childNodes].forEach(child => {
      /*
      	isElementNode(el){
          return el.nodeType === 1; 
        }
      */
      if(this.isElementNode(child)){
        // 元素节点
        // console.log('元素节点', child);
        this.compileElement(child);
      }else{
        // 非元素节点(文本节点...)
        // console.log('文本节点',child);
        this.compileText(child);
      }
      // 判断子节点下是否还有子节点,如果有则进行递归
      if(child.childNodes && child.childNodes.length){
        this.compile(child);
      }
    })
  }

编译元素节点

compileElement(node){
  	// 获取节点上的所有属性
    const attributes = node.attributes;
  	// 遍历属性数组
    [...attributes].forEach(attr => {
      // 使用解构赋值,将attr中的name属性和value属性,单独提取出来
      // 这里的name和value是和attr中的属性对应的,不能更改
      // name: v-text,v-model,v-bind....
      // vaule: v-text,v-model..后面跟的值
      const { name, value } = attr;
      /*
      	判断name是否以‘v-’开头,如果是,才进行编译,如果不是,则不进行处理
      	isDirective(attrName){
          return attrName.startsWith('v-');
        }
      */
      if(this.isDirective(name)){
        // 根据‘-’分割name,splite方法返回的是一个数组,第一个参数我们不用,第二个参数赋值给directive
        // 这里的directive和上面不一样,这里自己随意取个名字就好
        const [,directive] = name.split('-');
        // 因为指令不仅可能是v-text这种,还可能是v-on:click这种,所以需要进一步分割
        const [dirName,eventName] = directive.split(':');
        // 更新数据 数据驱动视图
        // 根据dirName的不同,调用complileUtil中不同的方法
        // dirName指的是text,html,bind,on...
        complileUtil[dirName](node, value, this.vm, eventName)

        // 删除有指令的标签上的属性
        node.removeAttribute(name);
      }
    })
  }

编译文本节点

compileText(node){
  // 获取文本内容
  const content = node.textContent;
  if(/\{\{(.+?)\}\}/.test(content)){
    complileUtil['text'](node,content,this.vm)
  }
}

complileUtil对象

内部实现了对不同指令的处理方法

const complileUtil = {
  getVal(expr, vm){
    /*
    	expr可能是msg 也可能是person.name,所以需要对expr再进行一次分割
    	
    	分割完得到一个数组,比如是['person', 'name'],使用reduce方法后
    	第一次返回vm.$data['person']
    	第二次返回vm.$data['person']['name']
    	
    	再比如是msg,则得到数组['msg'],所以直接返回vm.$data['msg']即可
    */
    return expr.split('.').reduce((data, currentVal) => {
      // console.log(data);
      // console.log(currentVal);
      return data[currentVal];
    }, vm.$data//初始值)
  },
  setVal(expr, vm, inputVal){
    return expr.split('.').reduce((data, currentVal) => {
      data[currentVal] = inputVal;
    }, vm.$data)
  },
  
	// 编译v-text指令以及文本节点的mustache语法{{msg}}
  text(node, expr, vm){
    let value;
    // 判断是否以‘{{’开头
    if(expr.indexOf("{{")!==-1){
      // 通过正则表达式 匹配双大括号,将大括号中的字符串提取出来
      value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
        // 传入getVal方法,在vm.$data中查找数据
        return this.getVal(args[1], vm);
      })
    }else{
      value = this.getVal(expr, vm);
    }
    // 更新视图
    this.updater.textUpdater(node, value);
  },
  // 编译v-html指令
  html(node, expr, vm){
    const value = this.getVal(expr, vm);
    this.updater.htmlUpdater(node, value);
  },
  // 编译v-model指令
  model(node, expr, vm){
    const value = this.getVal(expr, vm);
    // 视图=>数据=>视图
    // 监听input事件
    node.addEventListener('input', (e)=>{
      this.setVal(expr, vm, e.target.value);
    })
    this.updater.modelUpdater(node, value);
  },
  // 编译v-on指令
  on(node, expr, vm, eventName){
    let fn = vm.$options.methods && vm.$options.methods[expr];
    node.addEventListener(eventName, fn.bind(vm), false)
  },
  // 编译v-model指令
  bind(node, expr, vm, attrName){
    ...//这里大家可以自己动手去实现一下
  },
    
  //更新的函数
  updater: {
    textUpdater(node, value){
      node.textContent = value;
    },
    htmlUpdater(node, value){
      node.innerHTML = value;
    },
    modelUpdater(node, value){
      node.value = value;
    },
    bindUpdater(node, attrName, value){
      node[attrName] = value
    }
  }
}

实现一个数据监听器Observer

创建Observer类

作用

使用Object.defineProperty,为data中的所有属性设置getter和setter

class Observer{
  constructor(data){
    this.observer(data);
  }
  observer(data){
    if(data && typeof data === 'object'){
      // 遍历data对象
      Object.keys(data).forEach(key => {
        this.defineReactive(data, key, data[key]);
      })
    }
  }
  defineReactive(data, key, value){
    // 递归遍历
    this.observer(value);
    const dep = new Dep();
    // 监听并劫持所有的属性 
    // defineProperty传入data,key就是为了给data[key]添加对应的gettter和setter
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: false, //描述属性是否配置,以及可否删除
      get(){
        // 当访问属性时,会调用此函数
        // 初始化
        // 订阅数据发生变化时,往Dep中添加观察者
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      // 当属性值被修改时,会调用此函数
      // 这里使用箭头函数,是为了让this指向上层,而不是Object
      set: (newVal) => {
        this.observer(newVal)
        if(newVal !== value){
          value = newVal;
        }
        //告诉Dep通知变化
        dep.notify();
      }
    });
  }
}

在MVue类中实现

class MVue{
  constructor(options){
    this.$options = options;
    this.$el = options.el;
    this.$data = options.data;

    if(this.$el){
      // 实现一个数据观察者
      new Observer(this.$data);
      // 实现一个指令解析器
      new Compile(this.$el, this);
    }
  }
}

实现依赖收集器Dep

创建Dep类

作用

  • 收集观察者
  • 如果Observer中劫持的数据发生变化,会通知Dep去通知对应的观察者
class Dep{
  constructor() {
    this.subs = [];
  }
  // 收集观察者
  addSub(watcher){
    this.subs.push(watcher);
  }
  // 通知观察者更新
  notify(){
    this.subs.forEach(w => w.update())
  }
}

实现一个Watcher去更新视图

创建Watcher类

class Watcher{
  constructor(vm, expr, callback){
    this.vm = vm;
    this.expr = expr;
    this.callback = callback;
    this.oldVal = this.getOldVal()
  }
  getOldVal(){
    Dep.target = this;
    const oldVal = complileUtil.getVal(this.expr, this.vm)
    Dep.target = null;
    return oldVal;
  }
  update(){
    const newVal = complileUtil.getVal(this.expr, this.vm);
    if(newVal !== this.oldVal){
       this.callback(newVal);
    }
  }
}

在complileUtil的每个处理指令的方法中 实现Watcher

new Watcher(vm, expr, callback)

const complileUtil = {
  getContentVal(expr, vm){
    return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
      return this.getVal(args[1], vm)
    })
  },
  text(node, expr, vm){
    let value;
    if(expr.indexOf("{{")!==-1){
      value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
        new Watcher(vm, args[1], ()=>{
          this.updater.textUpdater(node, this.getContentVal(expr,vm));
        })
        return this.getVal(args[1], vm);
      })
    }else{
      value = this.getVal(expr, vm);
    }
    this.updater.textUpdater(node, value);
  },
  html(node, expr, vm){
    const value = this.getVal(expr, vm);
    // 绑定观察者,将来数据发生变化,触发这里的回调,进行更新
    new Watcher(vm, expr, (newVal)=>{
      this.updater.htmlUpdater(node, newVal);
    })
    this.updater.htmlUpdater(node, value);
  },
  ...
}

展示

到这,我们就实现了整个Vue的MVVM响应式,也实现了数据的双向绑定

300行代码实现Vue的MVVM响应式原理300行代码实现Vue的MVVM响应式原理

总结

vue是采用数据劫持配合发布者-订阅者模式的方式,通过
Object.defineProperty()来劫持各个属性的setter和getter,在数据变动时,发布消息给依赖收集器Dep,去通知观察者,做出对应的回调函数,去更新视图。

MVVM作为绑定的入口,整合了Observer,Compile和Watcher三者,通过Observer来监听model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起
Observer,Compile之间的通信桥梁,达到数据变化=>视图更新;视图交互变化=>数据model变更的双向绑定效果

如果文章有问题欢迎大家指出

参考教程:参考教程