Vue2响应式原理
333
2023-7-5

Vue通过Object.defineProperty来实现监听数据的改变和读取(属性中的getter和setter方法) 实现数据劫持。下面简单记录一下,vue监听数据变化的原理

let obj = {
		name:'lnj'
};

class Observer{
  // 只要将需要监听的那个对象传递给Observer这个类
  // 这个类就可以快速的给传入的对象的所有属性都添加get/set方法
	constructor(data){
    	this.observer(data)
    }
  observer(obj){
  	if(obj && typeof obj === 'object'){
      // 遍历取出传入对象的所有属性, 给遍历到的属性都增加get/set方法
    	for(let key in obj){
      	this.defineRecative(obj, key, obj[key])
      }
    }
  }
  defineRecative(obj, attr, value){
  	this.observer(value);
    Object.defineProperty(obj, attr, {
    	get(){
      	return value;
      },
      set: (newValue)=>{
      	if(value !== newValue){
        	this.observer(newValue);
          value = newValue;
           console.log('监听到数据的变化');
        }
      }
    })
  }
}

 new Observer(obj);
obj.name.a = 'data';

实现响应式

创建Vue实例

class Nue {
    constructor(options){
        // 1.保存创建时候传递过来的数据
        if(this.isElement(options.el)){
            this.$el = options.el;
        }else{
            this.$el = document.querySelector(options.el);
        }
        this.$data = options.data;
        // 2.根据指定的区域和数据去编译渲染界面
        if(this.$el){
            new Compiler(this)
        }
    }
    // 判断是否是一个元素
    isElement(node){
        return node.nodeType === 1;
    }
}
class Compiler {
    constructor(vm){
        this.vm = vm;
    }
}

提取元素到内存

创建一个Compiler类用于编译指令数据

class Compiler {
    constructor(vm){
        this.vm = vm;
        // 1.将网页上的元素放到内存中
        let fragment = this.node2fragment(this.vm.$el);
        console.log(fragment);
        // 2.利用指定的数据编译内存中的元素
        // 3.将编译好的内容重新渲染会网页上
    }
    node2fragment(app){
        // 1.创建一个空的文档碎片对象
        let fragment = document.createDocumentFragment();
        // 2.编译循环取到每一个元素
        let node = app.firstChild;
        while (node){
            // 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
            fragment.appendChild(node);
            node = app.firstChild;
        }
        // 3.返回存储了所有元素的文档碎片对象
        return fragment;
    }
}

查找指令和模板

在Compiler中,实现buildTemplate(处理模板)、buildElement(处理元素)、buildText(处理文本)这几个方法,对应处理指令和模板

class Compiler {
    constructor(vm){
        this.vm = vm;
        // 1.将网页上的元素放到内存中
        let fragment = this.node2fragment(this.vm.$el);
        // 2.利用指定的数据编译内存中的元素
        this.buildTemplate(fragment);
        // 3.将编译好的内容重新渲染会网页上
    }
    node2fragment(app){
        // 1.创建一个空的文档碎片对象
        let fragment = document.createDocumentFragment();
        // 2.编译循环取到每一个元素
        let node = app.firstChild;
        while (node){
            // 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
            fragment.appendChild(node);
            node = app.firstChild;
        }
        // 3.返回存储了所有元素的文档碎片对象
        return fragment;
    }
    buildTemplate(fragment){
        let nodeList = [...fragment.childNodes];
        nodeList.forEach(node=>{
            // 需要判断当前遍历到的节点是一个元素还是一个文本
            // 如果是一个元素, 我们需要判断有没有v-model属性
            // 如果是一个文本, 我们需要判断有没有{{}}的内容
            if(this.vm.isElement(node)){
                // 是一个元素
                this.buildElement(node);
                // 处理子元素(处理后代)
                this.buildTemplate(node);
            }else{
                // 不是一个元素
                this.buildText(node);
            }
        })
    }
    buildElement(node){
        let attrs = [...node.attributes];
        attrs.forEach(attr => {
            let {name, value} = attr;
            if(name.startsWith('v-')){
                console.log('是Vue的指令, 需要我们处理', name);
            }
        })
    }
    buildText(node){
        let content = node.textContent;
        let reg = /\{\{.+?\}\}/gi;
        if(reg.test(content)){
            console.log('是{{}}的文本, 需要我们处理', content);
        }
    }
}

编译指令和数据

定义一个CompilerUtil对象,对象的key,即是指令和数据的编译方法

let CompilerUtil = {
    getValue(vm, value){
       return value.split('.').reduce((data, currentKey) => {
            return data[currentKey];
        }, vm.$data);
    },
    model: function (node, value, vm) { 
        let val = this.getValue(vm, value);
        node.value = val;
    },
    html: function (node, value, vm) {
        let val = this.getValue(vm, value);
        node.innerHTML = val;
    },
    text: function (node, value, vm) {
        let val = this.getValue(vm, value);
        node.innerText = val;
    }
}

编译模板数据

let CompilerUtil = {
    getValue(vm, value){
       return value.split('.').reduce((data, currentKey) => {
            return data[currentKey.trim()];
        }, vm.$data);
    },
    getContent(vm, value){
        let reg = /\{\{(.+?)\}\}/gi;
        let val = value.replace(reg, (...args) => {
            return this.getValue(vm, args[1]);
        });
        // console.log(val);
        return val;
    },
  
   .......
  
    content: function (node, value, vm) {
        let val = this.getContent(vm, value);
        node.textContent = val;
    }
}

监听数据变化

class Observer{
  // 只要将需要监听的那个对象传递给Observer这个类
  // 这个类就可以快速的给传入的对象的所有属性都添加get/set方法
	constructor(data){
    	this.observer(data)
    }
  observer(obj){
  	if(obj && typeof obj === 'object'){
      // 遍历取出传入对象的所有属性, 给遍历到的属性都增加get/set方法
    	for(let key in obj){
      	this.defineRecative(obj, key, obj[key])
      }
    }
  }
  defineRecative(obj, attr, value){
  	this.observer(value);
    Object.defineProperty(obj, attr, {
    	get(){
      	return value;
      },
      set: (newValue)=>{
      	if(value !== newValue){
        	this.observer(newValue);
          value = newValue;
           console.log('监听到数据的变化');
        }
      }
    })
  }
}

数据驱动界面改变

// 想要实现数据变化之后更新UI界面, 我们可以使用发布订阅模式来实现
// 先定义一个观察者类, 再定义一个发布订阅类, 然后再通过发布订阅的类来管理观察者类
class Dep {
    constructor(){
        // 这个数组就是专门用于管理某个属性所有的观察者对象的
        this.subs = [];
    }
    // 订阅观察的方法
    addSub(watcher){
        this.subs.push(watcher);
    }
    // 发布订阅的方法
    notify(){
        this.subs.forEach(watcher=>watcher.update());
    }
}
class Watcher {
    constructor(vm, attr, cb){
        this.vm = vm;
        this.attr = attr;
        this.cb = cb;
        // 在创建观察者对象的时候就去获取当前的旧值
        this.oldValue = this.getOldValue();
    }
    getOldValue(){
        Dep.target = this;
        let oldValue = CompilerUtil.getValue(this.vm, this.attr);
        Dep.target = null;
        return oldValue;
    }
    // 定义一个更新的方法, 用于判断新值和旧值是否相同
    update(){
        let newValue = CompilerUtil.getValue(this.vm, this.attr);
        if(this.oldValue !== newValue){
            this.cb(newValue, this.oldValue);
        }
    }
}

完整示例

let CompilerUtil = {
  getValue(vm,value){
    return value.split('.').reduce((data, currentKey) =>{
      return data[currentKey.trim()];
    },vm.$data)
  },
  getContent(vm,value){
    let reg = /\{\{(.+?)\}\}/gi;// 提取大括号内容用 ()
    let val = value.replace(reg,(...args) =>{
      return this.getValue(vm,args[1]);
    });
    return val
  },
  setValue(vm, attr, newValue){
    attr.split('.').reduce((data, currentAttr, index, arr)=>{
      if(index === arr.length - 1){
        data[currentAttr] = newValue;
      }
      return data[currentAttr];
    }, vm.$data)
  },
  model:function (node,value,vm) {
    new Watcher(vm, value, (newValue, oldValue)=>{
      node.value = newValue;
      // debugger;
    });
    let val = this.getValue(vm,value);
    node.value = val;

    node.addEventListener('input', (e)=>{
      let newValue = e.target.value;
      this.setValue(vm, value, newValue);
    })
  },
  html:function (node,value,vm) {
    let val = this.getValue(vm,value);
    node.innerHTML = val;
  },
  text:function (node,value,vm) {
    let val = this.getValue(vm,value);
    node.innerText = val;
  },
  content(node,value,vm){
    let reg = /\{\{(.+?)\}\}/gi;
    let val = value.replace(reg, (...args)=>{
      new Watcher(vm, args[1], (newValue, oldValue)=>{
        node.textContent = this.getContent(vm, value);
      });
      return this.getValue(vm, args[1]);
    })
    node.textContent = val;
  },
  on(node, value, vm, type){
    node.addEventListener(type, (e)=>{
      vm.$methods[value].call(vm,e)
    })
  }
};

class Nue {
  constructor(option){
    // 1.保存创建时候传递过来的数据
    if (this.isElement(option.el)){
      this.$el = option.el;
    } else {
      this.$el = document.querySelector(option.el)
    }
    this.$data = option.data;
    this.proxyData();
    this.$methods = option.methods;
    this.$computed = option.computed;
    // 将computed中的方法添加到$data中,
    // 只有这样将来我们在渲染的时候才能从$data中获取到computed中定义的计算属性
    this.computed2data();
    // 2.根据指定的区域和数据去编译渲染界面
    if (this.$el){
      new Observer(this.$data);
      new Compiler(this)
    }
  }
  computed2data(){
    for (let key in this.$computed){
      Object.defineProperty(this.$data, key, {
        get: ()=>{
          return this.$computed[key].call(this)
        }
      })
    }
  }
  // 判断是否是一个元素
  isElement(node){
    return node.nodeType === 1
  }
  // 实现数据代理, 将$data上的数据添加到Vue实例上, 这样将来才能通过this.xxx直接获取数据
  proxyData(){
    for (let key in this.$data){
      Object.defineProperty(this, key,{
        get: () =>{
          return this.$data[key]
        }
      })
    }
  }

}
// 渲染方法
class Compiler {
  constructor(vm){
    this.vm = vm
    // 1.将网页上的元素放到内存中
    let fragment = this.node2fragment(this.vm.$el);
    // 2.利用指定的数据编译内存中的元素
    this.buildTemplate(fragment);
    // 3.将编译好的内容重新渲染会网页上
    this.vm.$el.appendChild(fragment);
  }
  node2fragment(app){
    let fragment = document.createDocumentFragment();
    let node = app.firstChild;
    while (node){
      fragment.appendChild(node);
      node = app.firstChild
    }
    return fragment
  }
  buildTemplate(fragment){
    let nodeList = [...fragment.childNodes];
    nodeList.forEach(node=>{
      // 需要判断当前遍历到的节点是一个元素还是一个文本
      // 如果是一个元素, 我们需要判断有没有v-model属性
      // 如果是一个文本, 我们需要判断有没有{{}}的内容
      if (this.vm.isElement(node)){
        // 是一个元素
        this.buildElement(node);
        // 处理子元素(处理后代)
        this.buildTemplate(node)
      } else {
        this.buildText(node)
      }
    })
  }
  buildElement(node){
    let attrs = [...node.attributes];
    attrs.forEach(attr =>{
      let { name, value } = attr;
      if (name.startsWith('@')){
        let [_, directiveType] = name.split('@');
        CompilerUtil['on'](node, value, this.vm, directiveType)
        node.removeAttribute(name);
      }
      if (name.startsWith('v-')){
       let [directiveName, directiveType] = name.split(':');
       let [_, directive] = directiveName.split('-');
       CompilerUtil[directive](node, value, this.vm,directiveType);
        node.removeAttribute(name); // 把自定义属性去掉
     }
    })
  }
  buildText(node){
    let content = node.textContent;
    let reg = /\{\{.+?\}\}/gi;
    if (reg.test(content)){
      CompilerUtil['content'](node, content, this.vm);
    }
  }
}
class Observer{
  constructor(data){
    this.observer(data)
  }
  observer(obj){
    if (obj && typeof obj === 'object'){
      for (let key in obj){
        this.defineRecative(obj,key,obj[key]) // 递归取出所有的key
      }
    }
  }
  defineRecative(obj, attr, value){
    this.observer(value);
    let dep = new Dep();
    Object.defineProperty(obj, attr, {
      get() {
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      set:(newValue) =>{
        if (value !== newValue){
          this.observer(newValue);
          value = newValue;
          dep.notify();
          console.log('需要更新');
        }
      }
    })
  }
}
// 想要实现数据变化之后更新UI界面, 我们可以使用发布订阅模式来实现
// 先定义一个观察者类, 再定义一个发布订阅类, 然后再通过发布订阅的类来管理观察者类
class Dep {
  constructor(){
    this.subs = []
  }
  addSub(watcher){
    this.subs.push(watcher)
  }
  notify(){
    this.subs.forEach(watcher => watcher.update());
  }
}
class Watcher {
  constructor(vm, attr, cb){
    this.vm = vm;
    this.attr = attr;
    this.cb = cb;
    this.oldValue = this.getOldValue()
  }
  getOldValue(){
    Dep.target = this;
    let oldValue = CompilerUtil.getValue(this.vm, this.attr);
    Dep.target = null;
    return oldValue;
  }
  update(){
    let newValue = CompilerUtil.getValue(this.vm, this.attr);
    if(this.oldValue !== newValue){
      this.cb(newValue, this.oldValue);
    }
  }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>01-Vue基本模板</title>
    <!--1.下载导入Vue.js-->
<!--    <script src="js/vue.js"></script>-->
    <script src="js/nue.js"></script>
</head>
<body>
<div id="app">
    <input type="text" v-model="name">
    <input type="text" v-model="time.h">
    <input type="text" v-model="time.m">
    <input type="text" v-model="time.s">
    <div v-html="html">abc</div>
    <div v-text="text">123</div>
    <p>{{ name }}</p>
    <p>{{age}}</p>
    <p>{{time.h}}</p>
    <p>{{name}}-{{age}}</p>
    <ul>
        <li>6</li>
        <li>6</li>
        <li>6</li>
    </ul>
</div>
<script>
    // 2.创建一个Vue的实例对象
    // let vue = new Vue({
    let vue = new Nue({
        // 3.告诉Vue的实例对象, 将来需要控制界面上的哪个区域
        el: '#app',
        // el: document.querySelector('#app'),
        // 4.告诉Vue的实例对象, 被控制区域的数据是什么
        data: {
            name: "lnj",
            age: 33,
            time: {
                h: 11,
                m: 12,
                s: 13
            },
            html: `<div>我是div</div>`,
            text: `<div>我是div</div>`
        }
    });
</script>
</body>
</html>