计算机系统应用教程网站

网站首页 > 技术文章 正文

vue2响应式原理 vue2.0响应式原理

btikc 2024-10-12 13:26:29 技术文章 11 ℃ 0 评论

Vue是一种现代的JavaScript框架,它以其简洁的语法和强大的功能在前端开发中广泛应用。其中最为核心和独特的特性之一就是响应式数据和双向绑定。

本篇文章将手把手带大家更清晰了解vue2响应式原理,让大家更深刻理解,也希望能给正在学习前端的朋友带来一点帮助。

引言

我们都知道vue2通过Object.defineProperty 方法响应式属性,在初始化实例的时候通过调用Observer数据监听函数,同时传入需要监听的数据,也就是$data属性;通过Compile函数去解析编译模版初始化我们的视图;最后通过Watcher记录组件中接触的数据为依赖,当依赖项被修改时通知wather,使他关联的组件重新渲染。

下面我们先看一下vue2里面的属性,接下来我们将仿照该实例自己书写一遍



Observer监听函数

首先我们将传进来的对象里data属性赋值给实例的$data,毕竟根据上图我们可以看出Vue也是用$data这种特殊的属性名称来表示我们传进来的data,接下来我们就可以进行数据劫持了。



在Javascript里,如果对象发生了变化就需要通知我们,这样我们就可以把更改后的属性更新到对象的DOM节点上,因此这里就需要调用数据监听函数Observer,这里我们就需要定义一个Observer函数,在调用该函数的时候我们还需要传入需要监听的数据,也就是实例$data属性。

既然是监听函数,那我们肯定就需要实现数据监听,这里我们就用到Object.defineProperty方法,而这个方法可以修改对象的现有属性它的第一个参数是要操作的对象,第二个参数是要操作的属性,最后传入一个对象并在对象里实现数据监听。在该对象中有几个属性,其中设置enumerable为true表示属性可以枚举;设置configurable为true表示属性描述符可以被改变;设置get函数表示访问该属性的时候会触发该函数;设置set函数表示属性值被修改的时候会触发该函数;



看到这里小伙伴们,到这里数据劫持就完成了,当然这里还是有问题的,如果给$data添加新的属性并且赋值一个新对象,这个新对象是没有get和set的,在Vue中不允许动态添加根级别的响应式不过Vue也给了我们解决方案,使用Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式,目前我们考虑的是Vue实例上原有数据暂时就先略过这一段。

接下来我们就需要将劫持以后的数据应用到我们的页面上了。

Compile解析模板

既然是要将劫持数据应用到页面上,我们就需要获取页面元素-应用Vue数据-渲染页面,但是我们不可能获取一个元素就渲染一次页面,我们需要减少对DOM的频繁操作,这里我们就需要增加一个步骤,获取页面元素后放入临时存储空间,把所有数据都更新后再渲染页面,接下来我们就创建一个Compile解析模板函数,对于该函数我们需要传入两个参数,第一个参数也就是我们实例里挂载的元素,第二个参数就是我们Vue的实例了。

讲到这里我先讲一下DocumentFragment,它是一个轻量级的文档,可以包含和控制节点,且不会像完整的文档那样占用额外资源。我们创建一个 DocumentFragment 并添加节点到里面,然后将这个 DocumentFragment 作为一个整体插入到DOM中,这样做比逐个插入节点效率会更高,接下来我们就可以把我们需要修改的内容应用到文档碎片中,在文档碎片中去修改我们的内容。



下面就是最关键的,当数据发生变动我们需要及时去更新视图

Watcher发布订阅者模式

Vue中使用的是发布订阅者模式,数据更新后通知所有已订阅过的实例去更新文档也就是收集依赖和通知订阅者。

首先我们需要创建一个新的类命名为depende后面都用dep表示,在dep中我们创建一个数组sub用来存放订阅者的信息,同时创建两个函数addSub和notify用来收集依赖和通知订阅者,然后我们再创建一个类名为Watcher的订阅者,并为其创建修改函数update,当我们修改文档内容的时候也就是我们模版解析将文本替换内容的时候我们就需要创建Watcher实例,告诉订阅者之后文本被修改后如何去更新。因为数据更新后我们需要去触发订阅者,所以当我们访问的时候就需要收集依赖添加订阅者到数组中去。这里我们就要在Object.defineProperty中访问的时候也就是get 中调用addSub,当数据被修改的时候调用notify通知订阅者调用update

下面代码可大家可自行取用,最后建议自己敲一下,更能深刻理解

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue2响应式原理</title>
</head>
<!-- <script src="https://cdn.jsdelivr.net/npm/vue@2"></script> -->
<script src="./vue.js"></script> 
<body>

    <div id="app">
        <p>性别:{{gender}}</p>
        <p>年龄:{{age}}</p>
        <p>姓名:{{name}}</p>
        <p>爱好:{{hobby}}</p>
        <p>地址:{{more.address}}</p>
    </div>
    <script>
        const vm = new Vue({
            el: "#app",
            data: {
                gender: "男",
                age: 23,
                name: "张三",
                hobby: ["吃饭", "睡觉", "打豆豆"],
                more:{
                    address:"杭州"
                }
            },
        })
        console.log(vm)
    </script>
</body>

</html>
class Vue {
  constructor(instance) {
    this.$data = instance.data;
    Observer(this.$data);
    Compile(instance.el, this);
  }
}
//数据劫持 - 监听实例数据
function Observer(data_instance) {
  //不是对象类型或者对象为空则返回
  if (!data_instance || typeof data_instance !== "object") return;
  //这里需要创建Dep,否者无法使用它的方法
  const dep = new Dep();
 
  Object.keys(data_instance).forEach((key) => {
    let value = data_instance[key];
    Observer(value); //这里是为了监听对象中的对象
    Object.defineProperty(data_instance, key, {
      enumerable: true,
      configurable: true,
      get() {
        //这里存放订阅者的信息
        if (Dep.target) dep.addSub(Dep.target); 
        return value;
      },
      set(newValue) {
        value = newValue;
        //这里监听对象被整个修改,新的属性并未被劫持,这里需要再次劫持
        Observer(newValue);
        //当数据被修改时候通知订阅者去更新
        dep.notify(key);
      },
    });
  });
}
//模版解析
function Compile(el, vm) {
  //获取挂载的元素
  vm.$el = document.querySelector(el);
  //创建文档碎片
  const fragment = document.createDocumentFragment();
  let child;
  //将挂载的元素添加到文档碎片中 ,遍历$el下的所有子元素,赋值给child,当child为空时,退出循环
  while ((child = vm.$el.firstChild)) {
    fragment.append(child);
  }
  fragment_update(fragment);
  //修改文档碎片中的内容
  function fragment_update(node) {
    //vue使用双大括号去转移,这里我们需要正则去匹配字符串
    const reg = /\{\{\s*(.*)\s*\}\}/;
    //当类型为3时,表示文本节点,这里我们使用正则去匹配文本节点中的{{}}
    if (node.nodeType === 3) {
        //因为文本被替换后,后面再次修改节点文本内容就不一样了,所以这里需要将原来的节点文本存下来
      const nodeValue = node.nodeValue;
      const result_regex = reg.exec(nodeValue);
      if (result_regex) {
        //因为我们data中的数据是对象类型,所以这里需要使用reduce方法去匹配对象中的属性
        const arr = result_regex[1].split(".");
        const reduce_value = arr.reduce((pre, cur) => pre[cur], vm.$data);
        //这里使用正则去替换文本,replace转义会将我们原本的复杂数据结构改变,这里我们将数据改为字符串同时去掉两遍字符串
        const value = JSON.stringify(reduce_value).replace(/^"|"$/g, "");
        node.nodeValue = nodeValue.replace(reg, value);
        //为每个需要解析的文档添加订阅者实例
        new Watcher(vm, result_regex[1], (newValue) => {
          node.nodeValue = nodeValue.replace(reg, newValue);
        });
      }
    }
    //因为节点里面还有可能会有节点,所以我们还需要用一下递归方法
    node.childNodes.forEach((child) => {
      fragment_update(child);
    });
  }
  vm.$el.appendChild(fragment);
}

//依赖 收集和通知订阅着
class Dep {
  constructor() {   
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  //通知订阅者更新视图
  notify(key) {
    const subItem = this.subs.find((sub) => sub.key === key);
    subItem.update();
  }
}
//订阅者
class Watcher {
  constructor(vm, key, callback) {
    this.vm = vm;
    this.key = key;
    this.callback = callback;
    //这里需要存放临时属性
    Dep.target = this;
    key.split(".").reduce((pre, cur) => pre[cur], vm.$data);
    Dep.target = null;
  }
  update() {
    const reduce_value = this.key
      .split(".")
      .reduce((pre, cur) => pre[cur], this.vm.$data);
    const value = JSON.stringify(reduce_value).replace(/^"|"$/g, "");
    this.callback(value);
  }
}


总结

Vue通过Observer监听数据的变化,通过Compile解析编译模板,最后由Watcher作为Observer和Compile两者之间通信。这样实现了当数据发生变化时,触发对应的Watcher实例去更新视图,在处理新增属性和数组问题的时候Vue也提供了我们Vue.set方法帮助我们解决。

最后,希望我的文章能给各位小伙伴面试或者在vue的项目中带来一点帮助。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表