【VUE】从源码角度说清楚MVVM!实现v-model!真的很简单!🔥

简介: 前言大家好,我是HoMeTown,今天聊一聊老生长谈的Vue之双向数据绑定。

前言

大家好,我是HoMeTown,今天聊一聊老生长谈的Vue之双向数据绑定

What?

首先我们先看什么是双向数据绑定

对于不是很了解设计模式的朋友,你可以先理解一下单向数据绑定,就是把数据绑定到视图,每次触发操作修改了数据视图就会更新,数据 -> 视图,可以理解为MV数据驱动视图

举个🌰:

网络异常,图片无法展示
|

通过点击按钮set name,触发点击事件,手动更新变量name的值为HoMeTown,但是当我改变input输入框里的值,变量 name的值却不变,如下图:

网络异常,图片无法展示
|

那么双向数据绑定就是在单向的基础上,通过操作更新视图数据自动更新,那上面的🌰来讲,就是我输入Input,变量name的值动态改变。视图 -> 数据,可以理解为VM视图驱动数据

How?

Vue中的双向数据绑定由三个重要部分组成:

  • 数据层(Model):应用的数据及业务逻辑
  • 视图层(View):应用的展示效果,各类UI组件
  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来

这个分层的架构方案,用专业术语来讲就是MVVM

ViewModel

ViewModel干了两件事儿:

  • 数据变化后更新视图
  • 视图变化后更新数据

它由两个重要部分组成:

  • 监听器(Ovserver):对所有数据的属性进行监听
  • 解析器(Compiler):对每个元素节点的指令进行扫描解析,根据指令模板替换数据,以及绑定相应的更新函数

动手实现

要做什么

Vue中,双向数据绑定的流程为:

  • new Vue()执行初始化,对data执行响应化处理,这个过程发生在Observe
  • 对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile
  • 定义一个更新函数和Watcher,将来对应数据变化时,Watcher调用更新函数
  • 由于data的某个属性在视图中可能出现N次,所以每个属性都需要一个Dep来管理多个Watcher
  • data中的数据一旦发生变化,首先会找到对应的Dep,然后通知这个Dep下所有的Watcher执行更新函数

参考下图:

网络异常,图片无法展示
|

Do it

首先定义一个Vue类, 做三件事

  • 数据劫持
  • 属性代理
  • 模板编译
class Vue {
    constructor(options) {
        this.$data = options.data
        this.$options = options
        // 数据劫持
        observe(this.$data)
        // 属性代理
        proxy(this.$data)
        // 模板编译
        compile(el, this)
    }
}
复制代码

接下来开始实现observe函数,做三件事

  • 递归data,劫持每一个
  • getter的时候收集
  • setter的时候通知执行
function observe(obj) {
    // 递归终止条件
    if(!obj || typeof obj !== 'object') return // 是空的 && 不是一个对象
    Object.keys(obj).forEach( key => {
        // 当前key对应的value
        const value = obj[key]
        // value能到这里,有可能是object,需要递归劫持
        observe(value)
        // 为当前的key所对应的属性添加getter & setter
        Object.defineProrerty(obj, key, {
            // 当且仅当该属性的 `enumerable` 键值为 `true` 时,该属性才会出现在对象的枚举属性中。
            enumerable: true,
            // 当且仅当该属性的 `configurable` 键值为 `true` 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
            configurable: true,
            get() {
                // 将new出来的Watcher实例进行收集
                Dep.target ? dep.addSub(Dep.target) : null
            },
            set(newValue) {
                if( val !== newValue ) dep.notify() // 通知执行
            }
        })
    })
}
复制代码

接下来实现Dep类,做两件事儿:

  • 依赖收集
  • 通知执行
//依赖收集的类
class Dep {
    constructor() {
        // 所有Watcher实例存在这里
        this.subs = []
    }
    // 添加Watcher实例
    addSub(watcher) {
        this.subs.push(watcher)
    }
    // 通知Watcher实例执行更新函数
    notify() {
        this.subs.forEach( w => w.update())
    }
}
复制代码

接下来实现订阅者Watcher类,做两件事:

  • 提供Dep.target
  • 提供更新数据的方法
class Watcher {
    // callback中,记录了当前watcher如何更新自己的文本内容
    // 与此同时,需要拿到最新的数据,所以,在new Watcher的时候,需要传递vm进来
    // 因为需要知道在vm很多属性中,哪个数据,才是当前自己所需要的数据,所以,new Watcher的时候,需要指定key
    constructor(vm, key, callback) {
        this.vm = vm
        this.key = key
        this.callback = callback
        // 把创建的watcher实例,在Dep.addSub时,存进Dep的subs里
        Dep.target = this; // 自定义target属性
        key.split(".").reduce((newobj, k) => newobj[k], vm);
        Dep.target = null;
    }
    // 发布者通知Watcher更新的方法
    update() {
        const value = this.key.split(".").reduce((newobj, k) => newobj[key], this.vm);
        this.callback(value)
    }
}
复制代码

最后实现compile,对HTML结构进行模板编译的方法:

function compile(el, vm) {
    // 获取elDom元素
    vm.$el = document.querySelector(el);
    // 创建文档碎片,提高Dom操作性能
    const fragment = document.createDocumentFragment();
    // 取出来
    while ((childNode = vm.$el.firstChild)) {
      fragment.appendChild(childNode);
    }
    // 进行模板编译
    replace(fragment)
    // 放进去呀
    vm.$el.appendChild(fragement)
    function replace(node) {
      // 定义匹配插值表达式的正则
      const regMustache = /\{\{\s*(\S+)\s*\}\}/;
      // \S匹配任何非空白字符
      // \s匹配任何空白字符,包括空格、制表符、换页符等等。
      // ()非空白字符提取出来,用一个小括号进行分组
      // 当前的node节点是一个文本子节点,需要进行替换
      if(node.nodeType == 3) {
          const text = node.textContent // 文本子节点的字符串内容
          const execResult = regMustache.exec(text) //为一个数组,索引为0的为{{name}},为1的为name,exec() 方法用于检索字符串中的正则表达式的匹配。
          if(execResult) {
              const value = execResult[1].split(".").reduce((newobj, k) => newobj[k], vm)
              node.textContent = text.replace(regMustache, value)
              // 此时,就可以创建Watcher实例,将这个方法存到watcher上,调用update就执行
              new Watcher(vm, execResult[1], (newValue) => {
                  node.textContent = text.replace(regMustache, newValue)
              });
              // good good
          }
          // 递归结束
          return
      } 
      // 判断当前的node节点是否为input输入框
      if(node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT' ){
          // 首先要做v-model,就得先拿到属性节点
          const attrs = Array.from(node.attributes);
          const findResult = attrs.find((x) => x.name === "v-model");
          if(findResult) {
              // 当前有v-model,获取值
              const expStr = findResult.value;
              const value = expStr.split(".").reduce((newobj, k) => newobj[k], vm);
              node.value = value;
              // 创建Watcher实例
              new Watcher(vm, expStr, (newValue) => {
                  node.value = newValue
              })
              // 监听input事件,拿到文本框最新的值,然后更新到vm上
              node.addEventListener("input", e => {
                  const keys = expStr.split(".")
                  const keysLen = keys.length
                  const obj = keys.slice(0, keysLen - 1).reduce((newobj, k) => newobj[k], vm);
                  obj[keys[keysLen - 1]] = e.target.value
              })
          }
      }
      // 走到这,证明不是文本节点,递归处理
      node.childNodes.forEach( child => replace(child))
    }
}
复制代码

测试

还是用最开始我们的那个🌰,修改如下:

HTML

<div id="app">
  <p>name:<span id="nameBox">{{name}}</span></p>
  <input v-model="name" id="ipt" type="text" />
  <button id="set">Set name</button>
</div>
复制代码

JS

const vm = new Vue({
    el: "#app",
    data: {
      name: "No name yet!",
    },
  });
  const setBtn = document.getElementById("set");
  setBtn.onclick = function () {
    vm.name = "Is HoMeTown!!";
  };
复制代码

点击按钮,修改Vue实例vm的属性name = 'Is HoMeTown!!'

网络异常,图片无法展示
|

可以看到已经成功了!这是单向,然后我们试一试,修改输入框的内容,上方name的值不会不跟着改变:

网络异常,图片无法展示
|

SUCCESS!!!!!!!

总结

Vue中,双向数据绑定的原理总结的来说有几点:

  • observe 进行数据劫持,getter时添加Watcher,setter时通知Watcher.update
  • Dep类实现 依赖收集与通知执行
  • Watcher类实现 订阅者执行更新
  • compile 进行模板编译,解析v-model,给input添加事件

完结~


目录
相关文章
|
6天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
6天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
6天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
6天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
|
JavaScript 容器
【Vue源码解析】mustache模板引擎
【Vue源码解析】mustache模板引擎
63 0
|
JavaScript 前端开发
vue源码解析之mustache模板引擎
vue源码解析之mustache模板引擎
104 0
|
JavaScript
01 - vue源码解析之vue 数据绑定实现的核心 Object.defineProperty()
01 - vue源码解析之vue 数据绑定实现的核心 Object.defineProperty()
85 0
|
JavaScript 索引
Vue $set 源码解析(保证你也能看懂)
说明这个key本来就在对象上面已经定义过了的,直接修改值就可以了,可以自动触发响应
124 0
Vue $set 源码解析(保证你也能看懂)
|
JavaScript 索引
Vue $set 源码解析
Vue $set 源码解析
103 0
Vue $set 源码解析
|
缓存 JavaScript 前端开发
Vue v-memo 指令的使用与源码解析
Vue v-memo 指令的使用与源码解析
167 0