Vue响应式原理简单实现

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

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


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

实现本身的Vue

简单功能介绍:node

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

2.视图交互变化,数据变动github

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

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

5.数据代理markdown



1.建立一个入口MVVM

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

2.Compile解析指令

咱们知道频繁的操做dom是很是消耗性能的,因此咱们须要将真实dom移入内存中操做--文档碎片。因此Compile主要作了一下几件事:dom

  • 将当前根节点全部子节点遍历放到内存中
  • 编译文档碎片,替换模板(元素、文本)节点中属性的数据
  • 将编译的内容回写到真实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当中,经过对源码的了解,是经过添加拦截器的方式,在数组和原型对象之间添加,改写了数组的七个方法,从而达到了响应式。


以上若是有什么错误以及不足的地方,请大佬们热心指出来。让咱们一块儿进步。

相关文章
相关标签/搜索