模拟 Vue.js响应式原理

news/2024/7/10 0:00:01 标签: vue

模拟 Vue.js响应式原理

  • 基本概念
    • 数据驱动
    • 响应式的核心原理
      • Vue 2.x
      • Vue 3.x
      • Object.defineProperty和Proxy的区别
    • 发布订阅模式和观察者模式
      • 发布订阅者模式
        • 模拟Vue自定义事件的实现(发布/订阅的实现)
      • 观察者模式
        • 模拟观察者模式实现
      • 总结
    • Vue响应式原理模拟-分析
      • Vue基本结构
      • 整体流程
    • Vue构造函数的实现
      • Vue构造函数options选项挂载到Vue实例上
      • Observer
        • defineReactive上=>为什么要传第三个参数
        • defineReactive下
          • 问题一: $data中有属性是对象
          • 问题二: $data中属性重新赋值,且赋值为一个新的对象,那么这个对象的成员是否是响应式的呢
      • Compiler(最重要的两个参数el:渲染的dom,vm:渲染所需要的数据)
        • isDirective(attrName),isTextNode(node), isElementNode(node)三个判断方法
        • compile(el)方法
        • compileText (node)
        • compileElement (node)
    • Dep => 订阅者
    • Watcher => 观察者
      • 创建Watcher对象1
      • 创建Watcher对象2
        • v-text指令
        • v-model指令
    • 双向数据绑定
    • Vue响应式原理代码

基本概念

  • 数据驱动
  • 响应式的核心原理
  • 发布订阅模式和观察者模式

数据驱动

  • 数据响应式,双向绑定,数据驱动
  1. 数据响应式
    1.1 数据模型仅仅是普通的JavaScript对象,而当我们修改数据时,视图会进行更新,避免了繁琐的 DOM 操作,提高开发效率
  2. 双向绑定
    2.1 数据改变,视图改变;视图改变,数据也随之改变
    2.2 我们可以使用v-model在表单元素上创建双向绑定
  3. 数据驱动,Vue最独特的特性之一
    3.1 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图

响应式的核心原理

Vue 2.x

  • 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter
  • Vue2.x数据响应式实现原理是Object.defineProperty,数据劫持set get

Vue 3.x

  • Vue3.x数据响应式实现原理是Proxy

Object.defineProperty和Proxy的区别

  • Object.defineProperty代理的是单个属性,如果要代理整个对象,那么要对整个对象的属性做循环遍历
  • Proxy代理的是整个目标对象(更优)

发布订阅模式和观察者模式

发布订阅者模式

  • 订阅者
  • 发布者
  • 信号中心
  • 我们规定,存在一个信号中心,某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscrible)这个信号,从而知道什么时候自己可以开始执行
  • 实例:Vue中的自定义事件=>兄弟组件之间的通信过程
  1. vm:信号中心
  2. vm.$on=>订阅
  3. vm.$emit=>发布
// eventBus.js
// 信号中心
let eventHub = new Vue()
// {"click":[fn1,fn2],"change":[fn]} // new Vue实例中有一个对象
// componentA.vue
// 发布者
addTodo: function () {
  // 发布消息(事件)
  eventHub.$emit("add-todo", { text: this.newTodoText })
  this.newTodoText = ""
}

// componentB.vue
// 订阅者
cteated: function () {
  // 订阅消息(事件)
  eventHub.$on("add-todo", this.addTodo)
}

模拟Vue自定义事件的实现(发布/订阅的实现)

  • 基本概念
  1. $on=>注册事件,在vm(信号中心)中定义一个变量对象存贮这些注册的事件及事件处理函数
    1.1 键: 事件名称
    1.2 值: 事件处理函数
    1.3 可以注册多个事件名称,也可以给同一个事件添加多个处理函数
  2. $emit =>执行对应事件的处理函数
  • 信号中心(类)有 o n 和 on和 onemit方法,还有一个对象存储$on注册的事件
  • 核心代码
 class EventEmitter {
      constructor() {
        // {"click":[fn1,fn2],"change":[fn]}
        this.subs = {};
      }
      // 注册事件
      $on(eventType, handler) {
        this.subs[eventType] = this.subs[eventType] || [];
        this.subs[eventType].push(handler);
      }
      // 触发事件
      $emit(eventType) {
        this.subs[eventType].forEach((handler) => {
          handler();
        });
      }
    }
  • 测试
   let em = new EventEmitter();
    // 为同一个事件名称添加多个处理函数
    em.$on("click", () => {
      console.log("click1");
    });
    em.$on("click", () => {
      console.log("click2");
    });
    em.$emit("click");
    // click1
    // click2

观察者模式

  • 观察者(订阅者)–watcher
    • update(): 当事件发生时,具体要做的事情(可以更新视图或者其他的操作)
  • 目标(发布者)–Dep (depdence依赖)
    • subs 数组: 存储所有的观察者
    • addSub(): 添加观察者
    • notify(): 当事件发生,调用所有观察者的update()方法
    • 原理:观察者模式没有信号中心,而是将订阅者(观察者)添加到发布者中,让发布者记录下所有的订阅者

模拟观察者模式实现

  • 核心代码
 // 发布者-目标
    class Dep {
      constructor() {
        // 记录所有的订阅者(观察者)watcher
        this.subs = [];
      }
      addSub(sub) {
        // 传递的对象必须有update方法,我们才能将该对象当成观察者
        if (sub.update) {
          this.subs.push(sub);
        }
      }
      notify() {
        this.subs.forEach((sub) => {
          sub.update();
        });
      }
    }
    // 订阅者-观察者
    class Watcher {
      update() {
        console.log("update");
      }
    }
  • 测试
    let dep = new Dep();
    let watcher = new Watcher();
    // 将订阅者添加到发布者中,让发布者记录下所有的订阅者
    dep.addSub(watcher);
    // 通知所有的订阅者,调用订阅者的update()方法
    dep.notify(); // update

总结

  • 观察者模式是由具体目标调度,比如当事件触发,Dep就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的

  • 发布/订阅者模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在,减少发布者和订阅者之间的依赖

    在这里插入图片描述

Vue响应式原理模拟-分析

Vue基本结构

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <p>姓名:{{name}}</p>
      <p>年龄:{{age}}</p>
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#app",
      data: {
        name: "szy",
        age: 20,
      },
    });
    console.log(vm);
  </script>
</html>
  • 我们观察打印的vm数据结构
  1. Vue构造函数内部是将data中的成员转换成get/set注入到Vue实例上,
    1.1 目的:在其他地方使用的时候我们可以直接通过this.name和this.age来
    使用,比较方便
    在这里插入图片描述

  2. Vue构造函数中的data选项成员记录到了$date中,并且转换成getter/setter
    2.1 目的 $data中的setter是真正监视数据变化的地方
    2.2 $data和_data指向的是同一个对象,_开头的是私有成员,$开头的是公共成员
    在这里插入图片描述

  3. 把Vue构造函数的参数记录到Vue实例的$options中
    在这里插入图片描述

  4. $el对应Vue构造函数选项中的el,可以是选择器或者dom对象

整体流程

在这里插入图片描述

Vue构造函数的实现

  • 功能
    • 负责接收初始化的参数(选项)
    • 负责把data中的属性注入到Vue实例,转换成getter/setter
    • 负责调用observer监听data中所有属性的变化
    • 负责调用compiler解析指令/插值表达式
  • 结构
  • $option
  • $el
  • $data
  • _proxyData();私有方法,将Vue构造函数data选项转换成getter/setter注入到Vue实例的$data中

Vue构造函数options选项挂载到Vue实例上

  • Vue构造函数核心代码 vue.js
class Vue {
  constructor(options) {
    // 1:通过属性保存选项的数据
    this.$options = options || {}
    this.$data = options.data || {}
    this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el
    // 2:把data中的成员转换成getter和setter,注入到Vue实例中
    this._proxyDate(this.$data)
    // 3:调用observer对象,监听数据的变化
    // 4:调用compiler对象,解析指令和插值表达式
  }
  _proxyDate (data) {
    // 遍历data中的所有属性,因为真正监控数据变化触发视图更新是$data的职责,这里只要Vue数据发生变化,通过set告诉了$data,就可以,所以Vm上的属性如果是对象,不需要对象的成员也是响应式的
    Object.keys(data).forEach(key => {
      // 把data的属性注入到Vue实例中
      Object.defineProperty(this, key, {
        configurable: true,
        enumerable: true,
        get () {
          return data[key]
        },
        set (newValue) {
        // newValue如果是对象,通知到$data就好,$data内部处理
          if (newValue === data[key]) {
            return
          }
          data[key] = newValue
        }
      })
    })
  }
}
  • Vue实例化对象
  const vm = new Vue({
    el: "#app",
    data: {
      name: "szy",
      age: 20,
    },
  });
  console.log(vm);

在这里插入图片描述

Observer

  • 功能

    • 负责把data选项中的属性转换成响应式数据=>将$data中的数据转换成getter和setter
    • data中的某个属性也是对象,把该属性也转换成响应式数据
    • 数据变化发送通知(调用数据的set方法时候发送通知)=>观察者模式来实现
  • 结构:两个方法

    • walk(data) 遍历data中所有属性
    • defineReactive(data,key,value) 定义响应式数据
  • Observer构造函数核心代码 observer.js

class Observer {
  constructor(data) {
    this.walk(data)
  }
  walk (data) {
    // 1:判断data是否是对象
    if (!data || typeof data !== "object") {
      return
    }
    // 2:遍历data对象的所有属性
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }
  defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
      configurable: true,
      enumerable: true,
      get () {
        return val
      },
      set (newValue) {
        if (newValue === val) {
          return
        }
        val = newValue
      }
    })
  }
}
  • vue.js中调用Observer=>将$data中的数据转换成getter和setter
class Vue {
  constructor(options) {
    // 1:通过属性保存选项的数据
    this.$options = options || {}
    this.$data = options.data || {}
    this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el
    // 2:把data中的成员转换成getter和setter,注入到Vue实例中
    this._proxyDate(this.$data)
    // 3:调用observer对象,监听数据的变化
    new Observer(this.$data)
    // 4:调用compiler对象,解析指令和插值表达式
  }
  _proxyDate (data) {
    // 遍历data中的所有属性
    Object.keys(data).forEach(key => {
      // 把data的属性注入到Vue实例中
      Object.defineProperty(this, key, {
        configurable: true,
        enumerable: true,
        get () {
          return data[key]
        },
        set (newValue) {
          if (newValue === data[key]) {
            return
          }
          data[key] = newValue
        }
      })
    })
  }
}

在这里插入图片描述

  • 在index.html中
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./observer.js"></script>
    <script src="./vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          name: "szy",
          age: 20,
        },
      });
      console.log(vm);
    </script>
  </body>
</html>

  • 现象
    在这里插入图片描述
  • this.defineReactive(data, key, data[key])有前两个值就够了,为什么还要传第三个值呢?后面解释

defineReactive上=>为什么要传第三个参数

  • 为什么defineReactive 要传第三个参数val=>现在解释,当我们不传递第三个参数参数的时候
    在这里插入图片描述
    在这里插入图片描述

  • 我们在控制台打印vm.name,实际的流程是

  1. this.name=>this.$data.name
    在这里插入图片描述
  2. this.$data.name=>val
    在这里插入图片描述

defineReactive下

问题一: $data中有属性是对象
  • 之前的name:“szy” age:20 data中的name和age属性都是原始值,这些属性是响应式的,那么data中有属性是对象的时候,这个对象里面的属性是否是响应式的呢
const vm = new Vue({
        el: "#app",
        data: {
          name: "szy",
          age: 20,
          // person属性是对象
          person: { 
            name: "zjl",
            age: 30,
          },
        },
      });
      console.log(vm);

在这里插入图片描述

  • d a t a . p e r s o n 中 的 n a m e 和 a g e 没 有 对 应 的 g e t t e r 和 s e t t e r , 所 以 data.person中的name和age没有对应的getter和setter,所以 data.personnameagegettersetter,data.person中的name和age不是响应式的,但我们调用defineReactive方法是将对象中所有的属性都转换成响应式的
defineReactive (obj, key, val) {
    // 如果Val是对象,把Val内部的属性转换成响应式数据
    this.walk(val)
    Object.defineProperty(obj, key, {
      configurable: true,
      enumerable: true,
      get () {
        return val
      },
      set (newValue) {
        if (newValue === val) {
          return
        }
        val = newValue
      }
    })

在这里插入图片描述

问题二: $data中属性重新赋值,且赋值为一个新的对象,那么这个对象的成员是否是响应式的呢
const vm = new Vue({
        el: "#app",
        data: {
          name: "szy",
          age: 20,
          person: {
            name: "zjl",
            age: 30,
          },
        },
      });
      // 重新赋值,那么test这个属性是否是响应式的呢?
      vm.name= { test: "hello" };
      console.log(vm);

在这里插入图片描述

  • 改进:新增的属性进行响应式处理
defineReactive (obj, key, val) {
    const that = this
    // 如果Val是对象,把Val内部的属性转换成响应式数据
    this.walk(val)
    Object.defineProperty(obj, key, {
      configurable: true,
      enumerable: true,
      get () {
        return val
      },
      set (newValue) {
        if (newValue === val) {
          return
        }
        // 新增的属性也要进行处理变成响应式的
        that.walk(newValue)
        val = newValue
      }
    })

在这里插入图片描述

  • 完整代码
class Observer {
  constructor(data) {
    this.walk(data)
  }
  walk (data) {
    // 1:判断data是否是对象
    if (!data || typeof data !== "object") {
      return
    }
    // 2:遍历data对象的所有属性
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }
  defineReactive (obj, key, val) {
    const that = this
    // 如果Val是对象,把Val内部的属性转换成响应式数据
    this.walk(val)
    Object.defineProperty(obj, key, {
      configurable: true,
      enumerable: true,
      get () {
        return val
      },
      set (newValue) {
        if (newValue === val) {
          return
        }
        // 新增的属性也要进行处理变成响应式的
        that.walk(newValue)
        val = newValue
      }
    })
  }
}

Compiler(最重要的两个参数el:渲染的dom,vm:渲染所需要的数据)

  • 功能:操作虚拟dom,为了方便演示,我们下面的实例中直接操作dom

    • 负责翻译模板,解析指令/插值表达式
    • 负责页面的首次渲染
    • 当数据发生变化后重新渲染视图
  • 结构

    • 属性
      • el: 模板对象
      • vm: Vue实例
    • 方法:下面方法都是在操作dom,当我们把所有的dom操作写在一个方法里,代码量会非常多,所以需要把dom操作的代码进行拆分,方便以后的维护
      • compile(el) : 遍历 dom 对象的所有节点(node),并且判断节点类型,如果是文本节点,则解析插值表达式,如果是元素节点,则解析指令
      • compileElement(node) : 编译元素节点,处理指令
      • compileText(node) : 编译文本节点,处理插值表达式
      • isDirective(attrName) : 判断当前属性是否是指令,在compileElement(node) 方法中调用
      • isTextNode(node) : 判断当前节点是否是文本节点 nodeType===3
      • isElementNode(node) ; 判断当前节点是否是元素节点 nodeType===1
  • 创建Compiler类 => compiler.js

isDirective(attrName),isTextNode(node), isElementNode(node)三个判断方法

class Compiler {
  constructor(vm) {
    this.el = vm.$el
    this.vm = vm
  }
  // 编译模板,处理文本节点和元素节点
  compile (el) {

  }
  // 编译元素节点,处理指令
  compileElement (node) {

  }
  // 编译文本节点,处理插值表达式
  compileText (node) {

  }
  // 判断元素属性是否是指令
  isDirective (attrName) {
    // 因为在vue中,所有的指令都是以v-开头
    return attrName.startsWith("v-")

  }
  // 判断节点是否是文本节点
  isTextNode (node) {
    return node.nodeType === 3
  }
  // 判断节点是否是元素节点
  isElementNode (node) {
    return node.nodeType === 1
  }
}

compile(el)方法

  • 遍历 dom 对象的所有节点(node),并且判断节点类型,如果是文本节点,则解析插值表达式,如果是元素节点,则解析指令
 // 编译模板,处理文本节点和元素节点
  compile (el) {
  	// 因为最终el会整体替换,只对它的子节点做处理,不对el本身处理,所以处理的是el.childNodes
    const childNodes = el.childNodes // children是元素节点
    // childNodes是伪元素
    Array.from(childNodes).forEach(node => {
      // 处理文本节点
      if (this.isTextNode(node)) {
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        // 处理元素节点
        this.compileElement(node)
      }
      // 判断node节点,是否有子节点,如果有子节点,要递归调用compile
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }

compileText (node)

  • 编译文本节点,处理插值表达式
  • 在compiler.js中打印文本节点
  constructor(vm) {
    this.el = vm.$el
    this.vm = vm
    // 调用this.compile方法
    this.compile(this.el)
  }
  // 编译文本节点,处理插值表达式
  compileText (node) {
    // 以对象的形式打印
    console.dir(node)
  }
  • 在index.html中导入compiler.js类
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <h1>插值表达式</h1>
      <h3>{{name}}</h3>
      <h3>{{age}}</h3>
      <h1>v-text</h1>
      <div v-text="name"></div>
      <h1>v-model</h1>
      <input type="text" v-model="name" />
      <input type="text" v-model="age" />
    </div>
    <script src="./compiler.js"></script>
    <script src="./observer.js"></script>
    <script src="./vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          name: "szy",
          age: 20,
          person: {
            name: "zjl",
            age: 30,
          },
        },
      });
      // 重新赋值,那么test这个属性是否是响应式的呢?
      vm.name = { test: "hello" };
      console.log(vm);
    </script>
  </body>
</html>

  • 在Vue类中调用compiler对象
class Vue {
  constructor(options) {
    // 1:通过属性保存选项的数据
    this.$options = options || {}
    this.$data = options.data || {}
    this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el
    // 2:把data中的成员转换成getter和setter,注入到Vue实例中
    this._proxyDate(this.$data)
    // 3:调用observer对象,监听数据的变化
    new Observer(this.$data)
    // 4:调用compiler对象,解析指令和插值表达式
    new Compiler(this)
  }
  _proxyDate (data) {
    // 遍历data中的所有属性
    Object.keys(data).forEach(key => {
      // 把data的属性注入到Vue实例中
      Object.defineProperty(this, key, {
        configurable: true,
        enumerable: true,
        get () {
          return data[key]
        },
        set (newValue) {
          if (newValue === data[key]) {
            return
          }
          data[key] = newValue
        }
      })
    })
  }
}
  • 页面显示内容
    在这里插入图片描述
  • 控制台打印内容,通过textContent判断文本节点的类型(换行,普通文本,插值表达式)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 正则匹配-替换
// 编译文本节点,处理插值表达式
  compileText (node) {
    // console.dir(node)
    // 匹配,提取插值 以 {{  name  }} 为例
    // node.textContent不仅能获取还能赋值
    let reg = /\{\{(.+?)\}\}/
    let value = node.textContent
    if (reg.test(value)) {
      let key = RegExp.$1.trim() //去除前后空格,此时的key的值就是 "name"
      node.textContent = value.replace(reg, this.vm[key])
      // 为什么不能用node.textContent=this.vm[key],因为插值表达式中可能有其他非变量字符
    }
  }

在这里插入图片描述

compileElement (node)

  • 编译元素节点,处理指令(此处我们以v-text和v-model为例)
  • 打印所有的属性节点node.attributes,观察结构
 // 编译元素节点,处理指令
  compileElement (node) {
    // 获取所有的属性节点
    console.log(node.attributes)
  }

在这里插入图片描述
在这里插入图片描述

  • 核心代码
  // 编译元素节点,处理指令
  compileElement (node) {
    // 获取所有的属性节点
    // console.log(node.attributes)
    // 遍历所有的属性节点
    Array.from(node.attributes).forEach(attr => {
      // 判断是否是指令
      let attrName = attr.name
      if (this.isDirective(attrName)) {
        // v-text --> text 方便为每一个指令定义一个处理函数,减少if条件判断
        // 属性节点的属性名
        attrName = attrName.substr(2)
        // 属性节点的值也是vm对象的属性(key)
        let key = attr.value
        this.update(node, key, attrName)
      }
    })
  }
  update (node, key, attrName) {
    /* 
    因为下面的方法中的value是vm中属性对象的值,所以需要key,
    不同的指令需要不同的处理函数,所以需要attrName 
    */
    let updateFn = this[attrName + "Updater"]
    //updateFn有值(对应指令的处理函数存在)才调用
    updateFn && updateFn(node, this.vm[key])

  }
  // 处理v-text指令
  textUpdater (node, value) {
    node.textContent = value
  }
  // 处理v-model指令
  modelUpdater (node, value) {
    // 该节点必须是表单元素
    node.value = value
  }

在这里插入图片描述

  • 引入update函数的好处,以后添加一个自定义指令,添加其对应的处理方法即可,不影响其他的模块
  • 完整代码
class Compiler {
  constructor(vm) {
    this.el = vm.$el
    this.vm = vm
    this.compile(this.el)
  }
  // 编译模板,处理文本节点和元素节点
  compile (el) {
    const childNodes = el.childNodes // children是元素节点
    // childNodes是伪元素
    Array.from(childNodes).forEach(node => {
      // 处理文本节点
      if (this.isTextNode(node)) {
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        // 处理元素节点
        this.compileElement(node)
      }
      // 判断node节点,是否有子节点,如果有子节点,要递归调用compile
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }
  // 编译元素节点,处理指令
  compileElement (node) {
    // 获取所有的属性节点
    // console.log(node.attributes)
    // 遍历所有的属性节点
    Array.from(node.attributes).forEach(attr => {
      // 判断是否是指令
      let attrName = attr.name
      if (this.isDirective(attrName)) {
        // v-text --> text 方便为每一个指令定义一个处理函数,减少if条件判断
        // 属性节点的属性名
        attrName = attrName.substr(2)
        // 属性节点的值也是vm对象的属性(key)
        let key = attr.value
        this.update(node, key, attrName)
      }
    })
  }
  update (node, key, attrName) {
    /* 
    因为下面的方法中的value是vm中属性对象的值,所以需要key,
    不同的指令需要不同的处理函数,所以需要attrName 
    */
    let updateFn = this[attrName + "Updater"]
    //updateFn有值(对应指令的处理函数存在)才调用
    updateFn && updateFn(node, this.vm[key])

  }
  // 处理v-text指令
  textUpdater (node, value) {
    node.textContent = value
  }
  // 处理v-modle指令
  modelUpdater (node, value) {
    // 该节点必须是表单元素
    node.value = value
  }
  // 编译文本节点,处理插值表达式
  compileText (node) {
    // console.dir(node)
    // 匹配,提取插值
    // node.textContent不仅能获取还能赋值
    let reg = /\{\{(.+?)\}\}/
    let value = node.textContent
    if (reg.test(value)) {
      let key = RegExp.$1.trim()
      node.textContent = value.replace(reg, this.vm[key])
    }
  }
  // 判断元素属性是否是指令
  isDirective (attrName) {
    // 因为在vue中,所有的指令都是以v-开头
    return attrName.startsWith("v-")

  }
  // 判断节点是否是文本节点
  isTextNode (node) {
    return node.nodeType === 3
  }
  // 判断节点是否是元素节点
  isElementNode (node) {
    return node.nodeType === 1
  }
}

Dep => 订阅者

在这里插入图片描述

  • 功能

    • 收集依赖,添加观察者(Watcher)
    • 通知所有观察者
  • 结构

    • subs : 数组,存储Dep中所有的watcher(观察者)
    • addsub(sub) : 添加watcher(观察者)
    • notify() : 当数据发生变化的时候通知所有的观察者
  • 创建 Dep类 => dep.js

class Dep {
  constructor() {
    // 存储所有观察者
    this.subs = []
  }
  // 添加观察者
  addSub (sub) {
    // 判断sub是否是观察者(具有update方法)
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 发送通知
  notify () {
    // 遍历subs中所有的观察者,调用没有个观察者的update方法来更新视图
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
  • 核心原理:为每一个响应式数据创建一个dep对象,在使用响应式数据的时候,收集依赖(创建观察者对象),在数据发生变化的时候,通着每一个观察者,调用观察者的update()方法来更新视图
  • 因为Observer类中创建了响应式数据($data中的每个成员都有getter和setter),在Observer类中使用Dep
defineReactive (obj, key, val) {
    /* 
    为$date中的每一个成员属性创建一个dep对象,在get方法中收集依赖(调用dep的addsub()方法),
    在set方法中发送通知(调用dep的notify()方法)
    */
    // 负责收集依赖,并发送通知
    const dep = new Dep
    const that = this
    this.walk(val)
    Object.defineProperty(obj, key, {
      configurable: true,
      enumerable: true,
      get () {
        // Dep.target为观察者对象
        Dep.target && dep.addSub(Dep.target)
        return val
      },
      set (newValue) {
        if (newValue === val) {
          return
        }
        that.walk(newValue)
        val = newValue
        // 发送通知
        dep.notify()
      }
    })
  }
  • 上面只实现了Dep(订阅者),观察者Watcher(观察者)还没实现,下面实现Watcher

Watcher => 观察者

在这里插入图片描述

  • 功能

    • 当数据发生变化的时候,dep通知所有的Watcher实例更新视图
    • 自身实例化的时候往dep对象中添加自己
  • 结构

    • vm : Vue实例
    • key : Sdata中的成员属性
    • cb :不同的watcher对象更新视图的时候所做的事情是不一样的,所以需要回调函数(cb),指明如何更新视图
    • oldValue : update()方法中可以获取到newValue,两者相等,不更新视图,有了vm和key,可以得到oldValue
    • update() : 更新视图
  • 创建Watcher类 => watcher.js

class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm
    // data中属性的名称
    this.key = key
    // 回到函数负责视图更新
    this.cb = cb
    // 把watcher对象记录到Dep类的静态属性target
    Dep.target = this
    this.oldValue = vm[key] // 此处因为用到了$date属性触发了该属性的get方法,此时将该wtacher实例添加到了dep的subs依赖数组中,完成了添加watcher(观察者)的功能
    // 防止重复添加watcher,将Dep.target的指针指向空
    Dep.target = null
  }
  update () {
    const newValue = this.vm[this.key]
    if (this.oldValue === newValue) {
      return
    }
    // 将变化的属性传到cb中,局部更新
    this.cb(newValue)
  }
}
  • 下面介绍何时创建Watcher的实例化对象

创建Watcher对象1

  • watcher实例的update方法(更新视图)实际上是在操作dom,而dom的操作是在Compiler类中完成的
  • 更新视图的操作实际上就是在操作dom,而我们所有的dom操作,都在Compiler中,在Compiler我们要找到把数据渲染到dom的位置,也就是处理指令(compileElement)和插值表达式(compileText)的位置
  • 指令和插值表达式都是依赖数据的,而所有视图中依赖数据的位置我们都要创建一个watcher对象,当数据发生改变的时候,dep对象会通知所有的watcher对象去重新渲染视图,所以我们要在compileText,textUpdater,modelUpdater方法中创建watcher对象
  • 首先在compileText()方法中添加watcher
  // 编译文本节点,处理插值表达式
  compileText (node) {
    // console.dir(node)
    // 匹配,提取插值
    // node.textContent不仅能获取还能赋值
    let reg = /\{\{(.+?)\}\}/
    let value = node.textContent
    if (reg.test(value)) {
      let key = RegExp.$1.trim()
      node.textContent = value.replace(reg, this.vm[key])
      // 创建watcher对象,当数据发生变化更新视图
      new Watcher(this.vm, key, (newValue) => {
        node.textContent = newValue
      })
    }
  }
  • 在index.html中导入依赖,因为Watcher类中使用了Dep类,所以dep.js在前,且注意注释掉vm.name = { test: "hello" };影响代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <h1>插值表达式</h1>
      <h3>{{name}}</h3>
      <h3>{{age}}</h3>
      <h1>v-text</h1>
      <div v-text="name"></div>
      <h1>v-modle</h1>
      <input type="text" v-model="name" />
      <input type="text" v-model="age" />
    </div>
    <script src="./dep.js"></script>
    <script src="./watcher.js"></script>
    <script src="./compiler.js"></script>
    <script src="./observer.js"></script>
    <script src="./vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          name: "szy",
          age: 20,
          person: {
            name: "zjl",
            age: 30,
          },
        },
      });
      // 重新赋值,那么test这个属性是否是响应式的呢?
      // vm.name = { test: "hello" };
      console.log(vm);
    </script>
  </body>
</html>

  • 观察视图
    在这里插入图片描述

创建Watcher对象2

  • 在指令中创建watcher对象

v-text指令

 // 处理v-text指令
  textUpdater (node, value) {
    node.textContent = value
    new watcher(this.vm, key, (newvalue) => {
      node.textContent = newvalue
    })
  }
  • 但是我们在textUpdater函数中没有key变量,z在update方法中把key变量传过来
  update (node, key, attrName) {
    /* 
    因为下面的方法中的value是vm中属性对象的值,所以需要key,
    不同的指令需要不同的处理函数,所以需要attrName 
    */
    let updateFn = this[attrName + "Updater"]
    //updateFn有值(对应指令的处理函数存在)才调用
    updateFn && updateFn(node, this.vm[key],key) //新增key变量
  }
  • textUpdater内部this指向问题,因为textUpdater在update中是直接调用的,所以内部的this指向window=>call()方法
  update (node, key, attrName) {
    /* 
    因为下面的方法中的value是vm中属性对象的值,所以需要key,
    不同的指令需要不同的处理函数,所以需要attrName 
    */
    let updateFn = this[attrName + "Updater"]
    //updateFn有值(对应指令的处理函数存在)才调用
    updateFn && updateFn.call(this, node, this.vm[key], key)

  }

v-model指令

 // 处理v-modle指令
  modelUpdater (node, value, key) {
    // 该节点必须是表单元素
    node.value = value
    new Watcher(this.vm, key, (newvalue) => {
      node.value = newvalue
    })
  }

在这里插入图片描述

  • 上面只是实现了数据发生变化,视图更新,但还没实现视图发生变化,数据更新在这里插入图片描述
  • 接下的双向数据绑定处理(视图发生变化,数据更新)

双向数据绑定

-概念

  • 数据发生变化,更新视图

  • 视图发生变化,更新数据

  • 原理:当视图发生变化的时候,触发了元素的input事件,给Sdata绑定的元素注册input事件

// 处理v-modle指令
  modelUpdater (node, value, key) {
    // 该节点必须是表单元素
    node.value = value
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue
    })
    // 双向绑定
    node.addEventListener("input", () => {
      this.vn[key] = node.value
    })
  }

在这里插入图片描述

Vue响应式原理代码

  • index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <h1>插值表达式</h1>
      <h3>{{name}}</h3>
      <h3>{{age}}</h3>
      <h1>v-text</h1>
      <div v-text="name"></div>
      <h1>v-modle</h1>
      <input type="text" v-model="name" />
      <input type="text" v-model="age" />
    </div>
    <script src="./dep.js"></script>
    <script src="./watcher.js"></script>
    <script src="./compiler.js"></script>
    <script src="./observer.js"></script>
    <script src="./vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          name: "szy",
          age: 20,
          person: {
            name: "zjl",
            age: 30,
          },
        },
      });
      // 重新赋值,那么test这个属性是否是响应式的呢?
      // vm.name = { test: "hello" };
      console.log(vm);
    </script>
  </body>
</html>

  • dep.js
class Dep {
  constructor() {
    // 存储所有观察者
    this.subs = []
  }
  // 添加观察者
  addSub (sub) {
    // 判断sub是否是观察者(具有update方法)
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 发送通知
  notify () {
    // 遍历subs中所有的观察者,调用没有个观察者的update方法来更新视图
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
  • watcher.js
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm
    // data中属性的名称
    this.key = key
    // 回到函数负责视图更新
    this.cb = cb
    // 把watcher对象记录到Dep类的静态属性target
    Dep.target = this
    this.oldValue = vm[key]  // 此处因为用到了$date属性触发了该属性的get方法,此时将该wtacher实例添加到了dep的subs依赖数组中,完成了添加watcher(观察者)的功能
    // 防止重复添加watcher
    Dep.target = null
  }
  update () {
    const newValue = this.vm[this.key]
    if (this.oldValue === newValue) {
      return
    }
    // 将变化的属性传到cb中,局部更新
    this.cb(newValue)
  }
}
  • compiler.js
class Compiler {
  constructor(vm) {
    this.el = vm.$el
    this.vm = vm
    this.compile(this.el)
  }
  // 编译模板,处理文本节点和元素节点
  compile (el) {
    const childNodes = el.childNodes // children是元素节点
    // childNodes是伪元素
    Array.from(childNodes).forEach(node => {
      // 处理文本节点
      if (this.isTextNode(node)) {
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        // 处理元素节点
        this.compileElement(node)
      }
      // 判断node节点,是否有子节点,如果有子节点,要递归调用compile
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }
  // 编译元素节点,处理指令
  compileElement (node) {
    // 获取所有的属性节点
    // console.log(node.attributes)
    // 遍历所有的属性节点
    Array.from(node.attributes).forEach(attr => {
      // 判断是否是指令
      let attrName = attr.name
      if (this.isDirective(attrName)) {
        // v-text --> text 方便为每一个指令定义一个处理函数,减少if条件判断
        // 属性节点的属性名
        attrName = attrName.substr(2)
        // 属性节点的值也是vm对象的属性(key)
        let key = attr.value
        this.update(node, key, attrName)
      }
    })
  }
  update (node, key, attrName) {
    /* 
    因为下面的方法中的value是vm中属性对象的值,所以需要key,
    不同的指令需要不同的处理函数,所以需要attrName 
    */
    let updateFn = this[attrName + "Updater"]
    //updateFn有值(对应指令的处理函数存在)才调用
    updateFn && updateFn.call(this, node, this.vm[key], key)

  }
  // 处理v-text指令
  textUpdater (node, value, key) {
    node.textContent = value
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue
    })
  }
  // 处理v-modle指令
  modelUpdater (node, value, key) {
    // 该节点必须是表单元素
    node.value = value
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue
    })
    // 双向绑定
    node.addEventListener("input", () => {
      this.vm[key] = node.value
    })
  }
  // 编译文本节点,处理插值表达式
  compileText (node) {
    // console.dir(node)
    // 匹配,提取插值
    // node.textContent不仅能获取还能赋值
    let reg = /\{\{(.+?)\}\}/
    let value = node.textContent
    if (reg.test(value)) {
      let key = RegExp.$1.trim()
      node.textContent = value.replace(reg, this.vm[key])
      // 创建watcher对象,当数据发生变化更新视图
      new Watcher(this.vm, key, (newValue) => {
        node.textContent = newValue
      })
    }
  }
  // 判断元素属性是否是指令
  isDirective (attrName) {
    // 因为在vue中,所有的指令都是以v-开头
    return attrName.startsWith("v-")

  }
  // 判断节点是否是文本节点
  isTextNode (node) {
    return node.nodeType === 3
  }
  // 判断节点是否是元素节点
  isElementNode (node) {
    return node.nodeType === 1
  }
}
  • observer.js
class Compiler {
  constructor(vm) {
    this.el = vm.$el
    this.vm = vm
    this.compile(this.el)
  }
  // 编译模板,处理文本节点和元素节点
  compile (el) {
    const childNodes = el.childNodes // children是元素节点
    // childNodes是伪元素
    Array.from(childNodes).forEach(node => {
      // 处理文本节点
      if (this.isTextNode(node)) {
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        // 处理元素节点
        this.compileElement(node)
      }
      // 判断node节点,是否有子节点,如果有子节点,要递归调用compile
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }
  // 编译元素节点,处理指令
  compileElement (node) {
    // 获取所有的属性节点
    // console.log(node.attributes)
    // 遍历所有的属性节点
    Array.from(node.attributes).forEach(attr => {
      // 判断是否是指令
      let attrName = attr.name
      if (this.isDirective(attrName)) {
        // v-text --> text 方便为每一个指令定义一个处理函数,减少if条件判断
        // 属性节点的属性名
        attrName = attrName.substr(2)
        // 属性节点的值也是vm对象的属性(key)
        let key = attr.value
        this.update(node, key, attrName)
      }
    })
  }
  update (node, key, attrName) {
    /* 
    因为下面的方法中的value是vm中属性对象的值,所以需要key,
    不同的指令需要不同的处理函数,所以需要attrName 
    */
    let updateFn = this[attrName + "Updater"]
    //updateFn有值(对应指令的处理函数存在)才调用
    updateFn && updateFn.call(this, node, this.vm[key], key)

  }
  // 处理v-text指令
  textUpdater (node, value, key) {
    node.textContent = value
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue
    })
  }
  // 处理v-modle指令
  modelUpdater (node, value, key) {
    // 该节点必须是表单元素
    node.value = value
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue
    })
    // 双向绑定
    node.addEventListener("input", () => {
      this.vm[key] = node.value
    })
  }
  // 编译文本节点,处理插值表达式
  compileText (node) {
    // console.dir(node)
    // 匹配,提取插值
    // node.textContent不仅能获取还能赋值
    let reg = /\{\{(.+?)\}\}/
    let value = node.textContent
    if (reg.test(value)) {
      let key = RegExp.$1.trim()
      node.textContent = value.replace(reg, this.vm[key])
      // 创建watcher对象,当数据发生变化更新视图
      new Watcher(this.vm, key, (newValue) => {
        node.textContent = newValue
      })
    }
  }
  // 判断元素属性是否是指令
  isDirective (attrName) {
    // 因为在vue中,所有的指令都是以v-开头
    return attrName.startsWith("v-")

  }
  // 判断节点是否是文本节点
  isTextNode (node) {
    return node.nodeType === 3
  }
  // 判断节点是否是元素节点
  isElementNode (node) {
    return node.nodeType === 1
  }
}
class Vue {
  constructor(options) {
    // 1:通过属性保存选项的数据
    this.$options = options || {}
    this.$data = options.data || {}
    this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el
    // 2:把data中的成员转换成getter和setter,注入到Vue实例中
    this._proxyDate(this.$data)
    // 3:调用observer对象,监听数据的变化
    new Observer(this.$data)
    // 4:调用compiler对象,解析指令和插值表达式
    new Compiler(this)
  }
  _proxyDate (data) {
    // 遍历data中的所有属性
    Object.keys(data).forEach(key => {
      // 把data的属性注入到Vue实例中
      Object.defineProperty(this, key, {
        configurable: true,
        enumerable: true,
        get () {
          return data[key]
        },
        set (newValue) {
          if (newValue === data[key]) {
            return
          }
          data[key] = newValue
        }
      })
    })
  }
}

http://www.niftyadmin.cn/n/1499656.html

相关文章

声明数组接收数据并切割

string jobNumber context.Request["jobNumber"]; string[] arrayJobNumber jobNumber.Split(,); 转载于:https://www.cnblogs.com/illegal/p/6374101.html

Vue.js + Typescript实现的轻量级Dialog组件

VueDialogX的孵化是由于作者在工作性质决定的。 作者在日常的工作中&#xff0c;经常接到各种各样简单的H5页面需求&#xff0c;活动页需求 这些需求有着共同的一点&#xff0c;就是业务不算复杂&#xff0c;对于组件库中大量的组件无需用到 仅对Dialog、Toast有着高频需求。所…

VMware设置共享文件夹

2019独角兽企业重金招聘Python工程师标准>>> 1、确保您的虚拟机关机。 2、编辑虚拟机的配置&#xff0c;添加共享文件夹&#xff0c;并启用它。 3。启动客户操作系统比如CentOS 4。重要的是&#xff0c;打开一个终端窗口&#xff0c;cd /mnt/hgfs 如果该目录下没有共…

每周汇报(五)

回家过年&#xff0c;项目暂时停滞转载于:https://www.cnblogs.com/nullustc/p/6374727.html

支付宝通知页面notify_url、返回页面return_url

返回页面&#xff08;return_url文件&#xff09;工作原理 即&#xff1a;商户系统请求/支付宝响应交互模式 1、 构造请求数据 商户通过提供的接口代码示例&#xff0c;通过代码示例的规则&#xff0c;程序构造与运算得到sign加密结果以及构造后的请求给支付宝的数据集合。GET方…

Linux 如何选择要kill掉的进程

从网上的找了一个比较全面的如下&#xff1a; OOM Killer在内存耗尽时&#xff0c;会查看所有进程&#xff0c;并分别为每个进程计算分数。将信号发送给分数最高的进程。 计算分数的方法 在OOM Killer计算分数时要考虑很多方面。首先要针对每个进程确认下列1&#xff5e;9个事…

Virtual DOM 的实现原理

Virtual DOM 的实现原理概念为什么使用Virtual DOM虚拟DOM的作用和虚拟DOM库虚拟DOM的作用虚拟DOM库Snabbdom基本使用创建项目导入Snabbdom代码演示模块Snabbdom 源码解析必备快捷键当前模块定位跨模块查找函数定位h 函数h函数源码h函数的三个私有变量传入三个参数传入两个参数…

【noip 2016】 蚯蚓(50分)(earthworm)

50分小程序&#xff0c;写了2天… 题目在这里 一个单调队列&#xff0c;写的都是p0的点&#xff0c;考试的时候要是弄到这些分的话……不说了…… 羡慕AC的神犇啊&#xff0c;54行的满分程序&#xff0c;而我…… #include <iostream> #include <cstdio> #includ…