前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Vue响应式系统原理

Vue响应式系统原理

原创
作者头像
yyds2026
发布于 2022-12-12 03:20:24
发布于 2022-12-12 03:20:24
42900
代码可运行
举报
文章被收录于专栏:前端开发面经前端开发面经
运行总次数:0
代码可运行

这一章就着重讲两个点:

  • 响应式系统如何收集依赖
  • 响应式系统如何更新视图 我们知道通过Object.defineProperty做了数据劫持,当数据改变的时候,get方法收集依赖,进而set方法调用dep.notify方法去通知Watcher调用本身update方法去更新视图。那么我们抛开其他问题,就讨论getnotifyupdate等方法,直接上代码:

get( )

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    }

我们知道Dep.target在创建Watcher的时候是null,并且它只是起到一个标记的作用,当我们创建Watcher实例的时候,我们的Dep.target就会被赋值到Watcher实例,进而放入target栈中,我们这里调用的是pushTarget函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 将watcher实例赋值给Dep.target,用于依赖收集。同时将该实例存入target栈中
export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

那我们继续执行到if (Dep.target)语句的时候就会调用Dep.depend函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 // 将自身加入到全局的watcher中
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

那下面的childOb是啥东西呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  let childOb = !shallow && observe(val)

我们通过这个变量判断当前属性下面是否还有ob属性,如果有的话继续调用Dep.depend函数,没有的话则不处理。

我们还需要处理当前传入的value类型,是数组属性的话则会调用dependArray收集数组依赖

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 收集数组依赖
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
  e = value[i]
  e && e.__ob__ && e.__ob__.dep.depend()
  if (Array.isArray(e)) {
    dependArray(e)
  }
}
}

那么收集依赖部分到这里就完了现在进行下一步触发更新

set( )

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
   set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // 判断NaN的情况
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }

我们看到了下面的 set函数触发了dep.notify()方法

notify( )

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  // 通知所有订阅者
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

notify里面我们就做了一件事情,遍历subs数组里面的所有Watcher,逐一调用update方法,也就是我们说的通知所有的订阅者Watcher调用自身update方法 update( )

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  update () {
    if (this.lazy) {
      // 计算属性会进来这段代码块
      // 这里将dirty赋值为true
      // 也不会马上去读取值
      // 当render-watcher的update被触发时
      // 重新渲染页面,计算属性会重新读值
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

那么update方法实现了什么呢?lazydirtysync又是啥?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
   if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
     // 这里将lazy的值赋值给了dirty
    // 就是说实例化的时候dirty = lazy = true
    this.dirty = this.lazy // for lazy watchers

那是控制计算属性的,当render—watcher的方法update被调用的时候,this.dirty会变为true会重新计算computed值,渲染视图,我们这里不叙述。

那么我们直接看queueWatcher()函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
 has[id] = true
 if (!flushing) {
   queue.push(watcher)
 } else {
   // if already flushing, splice the watcher based on its id
   // if already past its id, it will be run next immediately.
   let i = queue.length - 1
   while (i > index && queue[i].id > watcher.id) {
     i--
   }
   queue.splice(i + 1, 0, watcher)
 }
 // queue the flush
 if (!waiting) {
   waiting = true
   nextTick(flushSchedulerQueue)
 }
}
}

我们可以看到一个更新队列,更新队列指向:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated')
    }
  }
}

我们的callback调用updated钩子

讲到这里就有点超纲了,咱们初始化渲染会调用一个initRender函数创建dom,还有上面所述的nextTick,后期都会讲,那么了解了更新机制,下一章我们就来实现一个让面试官都惊呆了双向绑定

我们对Vue响应式系统有一定的了解,并且知道它是如何实现数据更新视图视图改变数据的,那么有这样的基础,我们来手写一个MVVM,以便面试的时候,吊打面试官(此为笑谈,不足论,嘿嘿)。

那么先抛出一张在座的各位再也熟悉不过的图:

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

1、当我们new MVVM之后有两步操作,Observer,Compile,我们知道Observer是做数据劫持,Compile是解析指令,那么问题来了:

  • Observer为什么要做数据劫持?
  • Compile为什么要做解析指令? 带着这两个问题,我们回顾一下往期内容:
  • 什么是数据响应式
  • 数据响应式原理是什么?
  • 数据响应式是如何实现的?

参考 Vue面试题详细解答

数据响应式就是数据双向绑定,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新;如果用户更新了View,那么Model数据也被自动更新了,这种情况就是双向绑定。

数据响应式原理

  • Vue实现数据响应式原理就是通过Object.defineProperty()这个方法重新定义了对象获取属性值get设置属性值set的操作来实现的
  • Vue3.0中是通过ECMAScript6中的proxy对象代理来实现的。 那么本章节就是来实现数据响应式的。

那么回答前面的两个问题,为什么要劫持数据?为什么要解析指令

  • 只有劫持到数据,才能对数据做到监听,以便于数据更改能够及时做到更新视图。
  • Vue中自定义了N多指令,只有解析它,我们JavaScript才能认识它,并运行它。 诸如此类问题我们不再复述,下面开始实现数据响应式。

写一个demo之前,我们应当整理好思路:

代码语言:markdown
AI代码解释
复制
1. 首先实现整体的一个架构(包括MVVM类或者VUE类、Watcher类),   /这里用到一个订阅发布者设计模式。
2. 然后实现MVVM中的由M到V,把模型里面的数据绑定到视图。
3. 最后实现V-M, 当文本框输入文本的时候,由文本事件触发更新模型中的数据
4. 同时也更新相对应的视图。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//html代码
<div id="app">
      <h1>MVVM双向绑定</h1>
      <div>
        <div v-text="myText"></div>
        <div v-text="myBox"></div>
        <input type="text" v-model="myText" />
        <input type="text" v-model="myBox" />
      </div>
</div>

我们创建了两个divinput实现input框数据关联,说白了也就是相同的数据源,那我们的数据源在哪呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//数据源data
const app = new Vue({
        el: "#app",
        data: {
          myText: "大吉大利!今晚吃鸡!",
          myBox: "我是一个盒子!",
        },
});

可见我们需要一个Vue类,也就是一个发布者,那么直接上代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//Vue类(发布者)
class Vue{

}

发布者有了,我们还需要有订阅者:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//Watcher类(订阅者)
class Watcher{

}

可见两者都有了,那么我们该怎么实现呢?

  • 获取data数据
  • 获取元素对象
  • 构造一个存放订阅者的对象
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 class Vue {
        constructor(optios) {
          this.$data = optios.data; //获取数据
          this.$el = document.querySelector(optios.el); //获取元素对象
          this._directive = {}; // 存放订阅者
        }
 }       

那么我们说了,我们需要劫持数据解析指令,那么我们得构造两个方法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
   class Vue {
       constructor(optios) {
         this.$data = optios.data; //获取数据
         this.$el = document.querySelector(optios.el); //获取元素对象
         this._directive = {}; // 存放订阅者
         this.Observer(this.$data);
         this.Compile(this.$el);
       }
       //劫持数据
       Observer(data) {
           Object.defineProperty(this.$data, key, {
             get: function(){},
             set: function(){}
             },
           });
       }
       //解析指令   //视图 --- >对象 -- >指令
       Compile(el) {

       }
   }

一个是劫持数据,一个是解析元素指令,劫持到的属性要根据属性分配容器,当当前容器不存在该属性的时候,我们便需要把他添加到订阅器对象里面,等待通知更新。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  for (let key in data) {
          this._directive[key] = [];
          let val =data[key];
          let watch = this._directive[key];
  }

那么解析指令,首先必须要递归当前节点,是否还有子节点,是否有v-text指令,v-model指令。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
       let nodes = el.children;
          for (let i = 0; i < nodes.length; i++) {
            let node = nodes[i];
            //递归 查询所有当前对象子类是否再含有子类
            if (node.children.length) {
              this.Compile(nodes[i]);
            }
            //判断是否含有V-text指令
            if (node.hasAttribute("v-text")) {
              let attrVal = node.getAttribute("v-text");

              this._directive[attrVal].push(
                new Watcher(node, this, "innerHTML", attrVal)
              );
            }

            //判断是否含有V-model指令
            if (node.hasAttribute("v-model")) {
              let attrVal = node.getAttribute("v-model");

              this._directive[attrVal].push(
                new Watcher(node, this, "value", attrVal)
              );
              node.addEventListener("input", () => {
                //赋值到模型
                this.$data[attrVal] = node.value;
                // console.log(this.$data);
              });
            }
          }

那么我们触发更新时候需要收集依赖,我们直接吧收集到的依赖return出去

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 Object.defineProperty(this.$data, key, {
              get: function(){
                    return val;
              }
 }

那么我们订阅者长什么样呢?我们订阅者,接收当前元素信息,MVVM对象,标识,属性。并且需要构造一个更新方法update

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    class Watcher {
        constructor(el, vm, exp, attr) {
          this.el = el;
          this.vm = vm;
          this.exp = exp;
          this.attr = attr;
          this.update();
        }
        //更新视图
        update() {
          this.el[this.exp] = this.vm.$data[this.attr];
          //div.innerHTML/value = this.Vue.$data["myText/myBox"]
        }
    }

到这里已经快完成了,那么我们收集了依赖就要去,通知watcher去更新视图啊,那么来了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    Object.defineProperty(this.$data, key, {
              get: function(){
                    return val;
              },
              set: function(newVal){
                  if(newVal !== val){
                    val = newVal;
                    watch.forEach(element => {
                        element.update();  
                    });
                  }
              },
    });

做到这里,你就可以实现一个数据响应式了。

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

我们已经掌握了响应式原理,那我们开始着手Vue的另一个核心概念组件系统

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
Nginx总结(一)如何安装Nginx【详细教程】
以前写过一些Nginx的文章,但都是用到什么说什么,没有一个完整系统的总结。趁最近有时间,打算将Nginx相关的内容重新整理一下。nginx系列文章地址如下:https://www.cnblogs.com/zhangweizhong/category/1529997.html
章为忠学架构
2020/01/17
12.9K0
Nginx总结(一)如何安装Nginx【详细教程】
nginx 安装
2.在网上下nginx包上传至Linux(https://nginx.org/download/),也可以直接下载
samRsa
2025/02/24
1410
nginx 安装
CentOS安装Nginx
一、gcc安装     yum -y install gcc-c++ 二、pcre pcre-devel安装     yum -y install pcre pcre-devel 三、zlib安装     yum -y install zlib zlib-devel 四、openSSL安装     yum -y install openssl openssl-devel 五、php-fpm安装     yum -y install php-fpm 六、下载Nginx安装包     wget htt
Sindsun
2018/04/28
1.7K0
Nginx | Java工程师必备课 之 Linux下安装Nginx
Nginx("engine x")是一款是由俄罗斯的程序设计师Igor Sysoev所开发高性能的 Web和 反向代理 服务器。也是一个 IMAP/POP3/SMTP 代理服务器。在高连接并发的情况下,Nginx是Apache服务器不错的替代品。 那么关于nginx在linux系统下如何安装,也是我们Java工程师的必备功课,接下来,我们直接上车,来看看,linux下是如何安装nginx的。 平台环境 虚拟机:VMware Workstation 系统:Linux:CentOS-7-x86_64 工具 安
码神联盟
2018/06/04
5570
CentOS7安装Nginx1.10.1
本篇文章来实战CentOS7环境下编译源码安装Nginx; 关闭防火墙(如果因为其他原因不关闭,也请不要禁止80端口):systemctl stop firewalld.service; 禁止防火墙自动启动:systemctl disable firewalld.service; 更新yum:yum update -y; 安装gcc:yum install -y gcc-c++; 安装pcre:yum install -y pcre pcre-devel; 安装zlib:yum install -y zli
程序员欣宸
2022/05/09
1910
Linux下安装和使用Nginx
Nginx (“engine x”) 是一个高性能的 HTTP和反向代理服务器,也是一个 IMAP/POP3/SMTP 服务器。 正向代理:
共饮一杯无
2022/11/24
1.3K0
Linux下安装和使用Nginx
Nginx系列:Nginx源码安装
Nginx是使用C语言开发的,建议部署在Linux操作系统上,当然也可以安装在Windows操作系统安装Windows版本的Nginx,本文演示在CentOS操作系统上安装源码安装Nginx。
BUG弄潮儿
2020/08/21
3.1K0
Nginx系列:Nginx源码安装
最新版Nginx安装教程来了,快来看看
  Nginx作为一款著名的Http服务器、反向代理服务器,在日常工作和学习中,我们都避免不了与它进行接触,本文通过亲自实践的方式,完整详细地讲解了nginx的安装流程,希望能够帮助到有需要的同学。
IT学习日记
2022/09/13
6710
最新版Nginx安装教程来了,快来看看
Linux(Centos)部署Nginx教程
Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器,Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,在BSD-like 协议下发行。其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等
Jensen_97
2023/12/19
2.2K0
Linux(Centos)部署Nginx教程
CentOS 6.4下安装Nginx 1.12.2
1.安装GCC 安装 nginx 需要先将官网下载的源码进行编译,编译依赖 gcc 环境,如果没有 gcc 环境,则需要安装
星哥玩云
2022/07/13
3120
Nginx教程
Nginx是一款高性能的http 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。由俄罗斯的程序设计师Igor Sysoev所开发,官方测试nginx能够支支撑5万并发链接,并且cpu、内存等资源消耗却非常低,运行非常稳定。
红目香薰
2022/11/29
5630
Nginx教程
Centos 7安装nginx并配置https[通俗易懂]
如果启动出现:nginx: [emerg] getpwnam(“www”) failed
全栈程序员站长
2022/09/01
5.2K0
Centos 7安装nginx并配置https[通俗易懂]
Nginx工作记录(1)——Centos7安装Nginx1.12.2
Nginx的下载网址为:http://nginx.org/en/download.html
胡了了
2019/05/29
8110
Nginx工作记录(1)——Centos7安装Nginx1.12.2
Nginx的编译
软件源码的处理(一般是删除,属于选做) 软件编译安装的注意事项: rpm和yum一起已经可以解决的软件,尽量不要自己编译安装。 软件的编译,一般需要编译环境以及一些响应的开发包,因此编译安装前需要将编译环境需要的软件安装上去。如:gcc gcc-c++ openssl-devel zlib-devel pcre pcre-devel等 先执行cd /usr/local/src/把要下载安装的文件下载到此路径下, 如果没有安装wget,要先下载wget yum install -y wget #下载'nj
企鹅号小编
2018/02/27
7600
Nginx的编译
CentOS 7 下安装 Nginx 原
Nginx 是 C语言 开发,建议在 Linux 上运行,当然,也可以安装 Windows 版本,本篇则使用 CentOS 7 作为安装环境。
拓荒者
2019/03/11
6620
CentOS 7 下安装 Nginx
                                                                            原
Linux编译安装Nginx1.16.1稳定版
centos 7 源码方式安装nginx(1.16.1) + ssl + 阿里证书配置
taixingyiji
2022/07/25
9310
Linux编译安装Nginx1.16.1稳定版
从零到一:手把手教你将项目部署上线-环境准备
小尘要自信
2024/05/26
9281
从零到一:手把手教你将项目部署上线-环境准备
Centos7 安装nginx1.16.0[通俗易懂]
nginx 使用C语言进行开发,建议在linux环境下运行,本文只介绍linux下的安装
全栈程序员站长
2022/06/27
8600
Centos7 安装nginx1.16.0[通俗易懂]
Nginx入门
是一个高性能的http和反向代理web服务器,同时提供了 IMAP、POP3、SMTP服务。
暴躁的程序猿
2022/03/23
2940
Nginx服务器
Nginx 是一款高性能的 http 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。由俄罗斯的程序设计师伊戈尔·西索夫(Igor Sysoev)所开发,官方测试 nginx 能够支支撑 5 万并发链接,并且 cpu、内存等资源消耗却非常低,运行非常稳定。
乐心湖
2021/01/18
1.2K0
Nginx服务器
相关推荐
Nginx总结(一)如何安装Nginx【详细教程】
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档