阅读 318

Vue响应式原理简单实现

简单阐述一下vue中的MVVM响应式原理:

#111复制代码

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


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

实现自己的Vue

简单功能介绍:

1.数据改变,视图自动更新

2.视图交互变化,数据变更

3.解析v-text,v-html指令

4.绑定v-on和@的事件

5.数据代理



1.创建一个入口MVVM

如图所示,MVVM要实现劫持监听所有属性和解析指令,同时也要实现数据代理功能


2.Compile解析指令

我们知道频繁的操作dom是非常消耗性能的,所以我们需要将真实dom移入内存中操作--文档碎片。所以Compile主要做了一下几件事:

  • 将当前根节点所有子节点遍历放到内存中
  • 编译文档碎片,替换模板(元素、文本)节点中属性的数据
  • 将编译的内容回写到真实DOM上
  • 添加wacther观察者到模板中渲染页面的表达式

'''
class Compile {  constructor(el, vm) {    // 判断el类型,得到dom对象    this.el = this.isElementNode(el) ? el : document.querySelector(el)    // 保存MVVM实例对象    this.vm = vm    // 获取文档碎片对象,存放内存中减少重绘和回流    const fragment = this.node2Frament(this.el)    // 编译模板    this.compile(fragment)    // 添加到根元素上    this.el.appendChild(fragment)  }  // 判断元素节点  isElementNode (node) {    return node.nodeType === 1  }  // 文档碎片对象  node2Frament (el) {    // 创建文档碎片对象    const f = document.createDocumentFragment()    let firstChild    // 将dom对象中的节点以此添加到f文档碎片对象    while (firstChild = el.firstChild) {      f.appendChild(firstChild)    }    return f  }  // 编译模板  compile (fragment) {    // 获取子节点    var childNodes = fragment.childNodes    // 转换数组遍历    childNodes = [...childNodes]    childNodes.forEach((child) => {      // 元素节点      if (child.nodeType === 1) {        // 处理元素节点        this.compileElement(child)      } else {        // 处理文本文本节点        this.compileText(child)      }      // 元素节点是否有子节点      if (child.childNodes && child.childNodes.length) {        // 递归        this.compile(child)      }    })  }  // 处理文本节点  compileText (node) {    // 获取文本信息    var content = node.textContent    const rgb = /\{\{(.+?)\}\}/    // 如果有{{}}表达式    if (rgb.test(content)) {      // 编译内容      compileUtil['text'](node, content, this.vm)    }  }  // 处理元素节点  compileElement (node) {    // 获取元素上属性    var attributes = node.attributes    attributes = [...attributes]    // 遍历属性    attributes.forEach((attr) => {      // 解构赋值获取属性名属性值      const { name, value } = attr;      // 判断属性名是否v-开头      if (this.isDirective(name)) {        //  分割字符串        const [, dirctive] = name.split('-')        // dirname 为html text等,eventName为事件名        const [dirName, eventName] = dirctive.split(':')        // 跟新数据 数据驱动视图        compileUtil[dirName](node, value, this.vm, eventName)        // 删除标签上的属性        node.removeAttribute('v-' + dirctive)      } else if (this.isEventive(name)) {             //@开头的事件        let [, eventName] = name.split('@')        compileUtil['on'](node, value, this.vm, eventName)        node.removeAttribute('@' + eventName)      }    })  }  isDirective (attrName) {    return attrName.startsWith('v-')  }  isEventive (eventName) {    return eventName.startsWith('@')  }}
'''复制代码

编译工具对象

通过Compile类解析了节点及文本各个指令和{{}}表达式,在通过编译工具数据与视图结合

```
class Compile {  constructor(el, vm) {    // 判断el类型,得到dom对象    this.el = this.isElementNode(el) ? el : document.querySelector(el)    // 保存MVVM实例对象    this.vm = vm    // 获取文档碎片对象,存放内存中减少重绘和回流    const fragment = this.node2Frament(this.el)    // 编译模板    this.compile(fragment)    // 添加到根元素上    this.el.appendChild(fragment)  }  // 判断元素节点  isElementNode (node) {    return node.nodeType === 1  }  // 文档碎片对象  node2Frament (el) {    // 创建文档碎片对象    const f = document.createDocumentFragment()    let firstChild    // 将dom对象中的节点以此添加到f文档碎片对象    while (firstChild = el.firstChild) {      f.appendChild(firstChild)    }    return f  }  // 编译模板  compile (fragment) {    // 获取子节点    var childNodes = fragment.childNodes    // 转换数组遍历    childNodes = [...childNodes]    childNodes.forEach((child) => {      // 元素节点      if (child.nodeType === 1) {        // 处理元素节点        this.compileElement(child)      } else {        // 处理文本文本节点        this.compileText(child)      }      // 元素节点是否有子节点      if (child.childNodes && child.childNodes.length) {        // 递归        this.compile(child)      }    })  }  // 处理文本节点  compileText (node) {    // 获取文本信息    var content = node.textContent    const rgb = /\{\{(.+?)\}\}/    // 如果有{{}}表达式    if (rgb.test(content)) {      // 编译内容      compileUtil['text'](node, content, this.vm)    }  }  // 处理元素节点  compileElement (node) {    // 获取元素上属性    var attributes = node.attributes    attributes = [...attributes]    // 遍历属性    attributes.forEach((attr) => {      // 解构赋值获取属性名属性值      const { name, value } = attr;      // 判断属性名是否v-开头      if (this.isDirective(name)) {        //  分割字符串        const [, dirctive] = name.split('-')        // dirname 为html text等,eventName为事件名        const [dirName, eventName] = dirctive.split(':')        // 跟新数据 数据驱动视图        compileUtil[dirName](node, value, this.vm, eventName)        // 删除标签上的属性        node.removeAttribute('v-' + dirctive)      } else if (this.isEventive(name)) {             //@开头的事件        let [, eventName] = name.split('@')        compileUtil['on'](node, value, this.vm, eventName)        node.removeAttribute('@' + eventName)      }    })  }  isDirective (attrName) {    return attrName.startsWith('v-')  }  isEventive (eventName) {    return eventName.startsWith('@')  }}
```复制代码

渲染工具对象

```
const updater = {  textUpdater (node, value) {    node.textContent = value  },  htmlUpdater (node, value) {    node.innerHTML = value  },  modelUpdater (node, value) {    node.value = value  }}
```复制代码

通过Compile及两个工具对象彻底将html上模板中的{{}}及指令转换为所需要的数据,完成了视图上解析指令--初始化视图

3.劫持监听所有属性Observer

通过Object,defineProperty的方法来劫持每个属性,当属性被修改时,会触发set函数,此时,我们要通知变化属性的每个订阅者(watcher)来改变视图在劫持每个属性的时候。

那每个订阅者又存在哪里呢,这时候我们需要一个dep实例来收集他们。

```
let uid = 0class Dep {  constructor() {    this.id = uid++    this.subs = []  }  // 收集观察者  addSub (watcher) {    this.subs.push(watcher)  }  // 通知观察者  notify () {    this.subs.forEach(w => w.update())  }}
```复制代码

每个dep实例都有一个数组来存放watcher,当数据发生改变,会触发每个实例的notify方法,watcher再去更新视图,达到了响应式。

```
defineReactive: function(data, key, val) {        var dep = new Dep();        var childObj = observe(val);        Object.defineProperty(data, key, {            enumerable: true, // 可枚举            configurable: false, // 不能再define            get: function() {                if (Dep.target) {                    dep.depend();                }                return val;            },            set: function(newVal) {                if (newVal === val) {                    return;                }                val = newVal;                // 新的值是object的话,进行监听                childObj = observe(newVal);                // 通知订阅者                dep.notify();            }        });    }```复制代码

那么watcher又是怎么存放到subs数组中的呢?

在Compile函数中,我们每次实现解析操作的时候,我们添加一个watcher实例

```
class watcher {  constructor(vm, expr, cb) {    this.vm = vm    this.expr = expr    this.cb = cb    this.oldVal = this.getOldVal()  }  // 获取旧值  getOldVal () {    Dep.target = this    const oldVal = compileUtil.getVal(this.expr, this.vm)    Dep.target = null    return oldVal  }  // 更新视图  update () {    const newVal = compileUtil.getVal(this.expr, this.vm)    if (this.oldVal !== newVal) {      this.cb(newVal)    }  }}```复制代码

在watcher实例里面绑定了一个更新函数cb,当调用update时候,就去执行更新函数到达渲染。

重点重点,dep和watcher产生关系

在初始模板的时候,在Compile里,每个watcher获取oldVal值时,会触发Object.defineProperty中get的方法,再触发get函数前,执行Dep.target=this,将watcher实例绑定到Dep.target,当我们执行get方法时,调用dep.addSub(Dep.target),就将watcher实例添加到了dep中的数组里了。当模板渲染完成时。模板里每个需要解析的数据都绑定了一个watcher,同时,每个watcher都被添加到了对应的dep中。


整个流程概述:

  1. 实例一个MVVM
  2. Observer劫持每个属性变成响应式,每个属性都会实例化一个dep
  3. Compile编译模板,需要解析指令和{{}}时,添加一个watcher实例
  4. watcher实例内部会去读取属性值,触发get方法,同时将watcher添加到dep里
  5. 数据更新,触发set方法,通知dep中所有收集的watcher,在通过watcher更新视图


项目github地址

https://github.com/6sy/vue-mvvm.git复制代码

总结

通过自己实现一下响应式原理,发现了自己还有很多需要走的路,在这项目中,是无法监听到数组变化的,而在vue当中,通过对源码的了解,是通过添加拦截器的方式,在数组和原型对象之间添加,改写了数组的七个方法,从而达到了响应式。


以上如果有什么错误以及不足的地方,请大佬们热心指出来。让我们一起进步。