https://www.bilibili.com/video/av51444410/?p=5

https://github.com/amandakelake/blog/issues/63

https://mp.weixin.qq.com/s/X3s4ysLfwclEOXIuKzOK2g

Vue 进阶系列之响应式原理及实现

前端大全 3/17
 

以下文章来源于高级前端进阶 ,作者木

高级前端进阶

木易杨,资深前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!

(给前端大全加星标,提升前端技能)

转自: 高级前端进阶

什么是响应式Reactivity

Reactivity表示一个状态改变之后,如何动态改变整个系统,在实际项目应用场景中即数据如何动态改变Dom。

需求

现在有一个需求,有a和b两个变量,要求b一直是a的10倍,怎么做?

简单尝试1:

let a = 3;let b = a * 10;console.log(b); // 30

乍一看好像满足要求,但此时b的值是固定的,不管怎么修改a,b并不会跟着一起改变。也就是说b并没有和a保持数据上的同步。只有在a变化之后重新定义b的值,b才会变化。

a = 4;console.log(a); // 4console.log(b); // 30b = a * 10;console.log(b); // 40

简单尝试2:

将a和b的关系定义在函数内,那么在改变a之后执行这个函数,b的值就会改变。伪代码如下。

onAChanged(() => {
   b = a * 10;
})

所以现在的问题就变成了如何实现onAChanged函数,当a改变之后自动执行onAChanged,请看后续。

结合view层

现在把a、b和view页面相结合,此时a对应于数据,b对应于页面。业务场景很简单,改变数据a之后就改变页面b。

<span class="cell b"></span>document
   .querySelector('.cell.b')
   .textContent = state.a * 10

现在建立数据a和页面b的关系,用函数包裹之后建立以下关系。

<span class="cell b"></span>onStateChanged(() => {    document
       .querySelector(‘.cell.b’)
       .textContent = state.a * 10})

再次抽象之后如下所示。

<span class="cell b">
   {{ state.a * 10 }}
</span> onStateChanged(() => {
   view = render(state)
})

view = render(state)是所有的页面渲染的高级抽象。这里暂不考虑view = render(state)的实现,因为需要涉及到DOM结构及其实现等一系列技术细节。这边需要的是onStateChanged的实现。

实现

实现方式是通过Object.defineProperty中的gettersetter方法。具体使用方法参考如下链接。

MDN之Object.defineProperty

需要注意的是getset函数是存取描述符,valuewritable函数是数据描述符。描述符必须是这两种形式之一,但二者不能共存,不然会出现异常。

实例1:实现convert()函数

要求如下:

  • 1、传入对象obj作为参数

  • 2、使用Object.defineProperty转换对象的所有属性

  • 3、转换后的对象保留原始行为,但在get或者set操作中输出日志

示例:

const obj = { foo: 123 }
convert(obj) obj.foo // 输出 getting key "foo": 123obj.foo = 234 // 输出 setting key "foo" to 234obj.foo // 输出 getting key "foo": 234

在了解Object.definePropertygettersetter的使用方法之后,通过修改getset函数就可以实现onAChangedonStateChanged

实现:

function convert (obj) {  // 迭代对象的所有属性
 // 并使用Object.defineProperty()转换成getter/setters
 Object.keys(obj).forEach(key => {  
   // 保存原始值
   let internalValue = obj[key]    
   Object.defineProperty(obj, key, {
     get () {        
      console.log(`getting key "${key}": ${internalValue}`)        return internalValue
     },
     set (newValue) {        
      console.log(`setting key "${key}" to: ${newValue}`)
       internalValue = newValue
     }
   })
 })
}

实例2:实现Dep

要求如下:

  • 1、创建一个Dep类,包含两个方法:dependnotify

  • 2、创建一个autorun函数,传入一个update函数作为参数

  • 3、在update函数中调用dep.depend(),显式依赖于Dep实例

  • 4、调用dep.notify()触发update函数重新运行

示例:

const dep = new Dep()

autorun(() => {
 dep.depend()  console.log('updated')
})// 注册订阅者,输出 updateddep.notify()// 通知改变,输出 updated

首先需要定义autorun函数,接收update函数作为参数。因为调用autorun时要在Dep中注册订阅者,同时调用dep.notify()时要重新执行update函数,所以Dep中必须持有update引用,这里使用变量activeUpdate表示包裹update的函数

实现代码如下。

let activeUpdate = null function autorun (update) {  const wrappedUpdate = () => {
   activeUpdate = wrappedUpdate    // 引用赋值给activeUpdate
   update()                        // 调用update,即调用内部的dep.depend
   activeUpdate = null             // 绑定成功之后清除引用
 }
 wrappedUpdate()                   // 调用}

wrappedUpdate本质是一个闭包,update函数内部可以获取到activeUpdate变量,同理dep.depend()内部也可以获取到activeUpdate变量,所以Dep的实现就很简单了。

实现代码如下。

class Dep {  // 初始化
 constructor () {          
   this.subscribers = new Set()
 }  // 订阅update函数列表
 depend () {    if (activeUpdate) {    
     this.subscribers.add(activeUpdate)
   }
 }  // 所有update函数重新运行
 notify () {              
   this.subscribers.forEach(sub => sub())
 }
}

结合上面两部分就是完整实现。

实例3:实现响应式系统

要求如下:

  • 1、结合上述两个实例,convert()重命名为观察者observe()

  • 2、observe()转换对象的属性使之响应式,对于每个转换后的属性,它会被分配一个Dep实例,该实例跟踪订阅update函数列表,并在调用setter时触发它们重新运行

  • 3、autorun()接收update函数作为参数,并在update函数订阅的属性发生变化时重新运行。

示例:

const state = {  count: 0}

observe(state)

autorun(() => {  console.log(state.count)
})// 输出 count is: 0state.count++// 输出 count is: 1

结合实例1和实例2之后就可以实现上述要求,observe中修改obj属性的同时分配Dep的实例,并在get中注册订阅者,在set中通知改变。autorun函数保存不变。 实现如下:

class Dep {  // 初始化
 constructor () {          
   this.subscribers = new Set()
 }  // 订阅update函数列表
 depend () {    if (activeUpdate) {    
     this.subscribers.add(activeUpdate)
   }
 }  // 所有update函数重新运行
 notify () {              
   this.subscribers.forEach(sub => sub())
 }
}function observe (obj) {  // 迭代对象的所有属性
 // 并使用Object.defineProperty()转换成getter/setters
 Object.keys(obj).forEach(key => {    let internalValue = obj[key]    // 每个属性分配一个Dep实例
   const dep = new Dep()    Object.defineProperty(obj, key, {    
     // getter负责注册订阅者
     get () {
       dep.depend()        return internalValue
     },      // setter负责通知改变
     set (newVal) {        const changed = internalValue !== newVal
       internalValue = newVal        
       // 触发后重新计算
       if (changed) {
         dep.notify()
       }
     }
   })
 })  return obj
}let activeUpdate = nullfunction autorun (update) {  // 包裹update函数到"wrappedUpdate"函数中,
 // "wrappedUpdate"函数执行时注册和注销自身
 const wrappedUpdate = () => {
   activeUpdate = wrappedUpdate
   update()
   activeUpdate = null
 }
 wrappedUpdate()
}

结合Vue文档里的流程图就更加清晰了。 

Job Done!!!

本文内容参考自VUE作者尤大的付费视频

《Vue 进阶系列之响应式原理及实现》的更多相关文章

  1. 简单物联网:外网访问内网路由器下树莓派Flask服务器

    最近做一个小东西,大概过程就是想在教室,宿舍控制实验室的一些设备. 已经在树莓上搭了一个轻量的flask服务器,在实验室的路由器下,任何设备都是可以访问的:但是有一些限制条件,比如我想在宿舍控制我种花 ...

  2. 利用ssh反向代理以及autossh实现从外网连接内网服务器

    前言 最近遇到这样一个问题,我在实验室架设了一台服务器,给师弟或者小伙伴练习Linux用,然后平时在实验室这边直接连接是没有问题的,都是内网嘛.但是回到宿舍问题出来了,使用校园网的童鞋还是能连接上,使 ...

  3. 外网访问内网Docker容器

    外网访问内网Docker容器 本地安装了Docker容器,只能在局域网内访问,怎样从外网也能访问本地Docker容器? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Docker容器 ...

  4. 外网访问内网SpringBoot

    外网访问内网SpringBoot 本地安装了SpringBoot,只能在局域网内访问,怎样从外网也能访问本地SpringBoot? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装Java 1 ...

  5. 外网访问内网Elasticsearch WEB

    外网访问内网Elasticsearch WEB 本地安装了Elasticsearch,只能在局域网内访问其WEB,怎样从外网也能访问本地Elasticsearch? 本文将介绍具体的实现步骤. 1. ...

  6. 怎样从外网访问内网Rails

    外网访问内网Rails 本地安装了Rails,只能在局域网内访问,怎样从外网也能访问本地Rails? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Rails 默认安装的Rails端口 ...

  7. 怎样从外网访问内网Memcached数据库

    外网访问内网Memcached数据库 本地安装了Memcached数据库,只能在局域网内访问,怎样从外网也能访问本地Memcached数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装 ...

  8. 怎样从外网访问内网CouchDB数据库

    外网访问内网CouchDB数据库 本地安装了CouchDB数据库,只能在局域网内访问,怎样从外网也能访问本地CouchDB数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Cou ...

  9. 怎样从外网访问内网DB2数据库

    外网访问内网DB2数据库 本地安装了DB2数据库,只能在局域网内访问,怎样从外网也能访问本地DB2数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动DB2数据库 默认安装的DB2 ...

  10. 怎样从外网访问内网OpenLDAP数据库

    外网访问内网OpenLDAP数据库 本地安装了OpenLDAP数据库,只能在局域网内访问,怎样从外网也能访问本地OpenLDAP数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动 ...

随机推荐

  1. 【西北师大-2108Java】第五次作业成绩汇总

    [西北师大-2108Java]第五次作业成绩汇总 作业题目 面向对象程序设计(JAVA) 第7周学习指导及要求 实验目的与要求 (1)掌握四种访问权限修饰符的使用特点: (2)掌握Object类的用途 ...

  2. AcWing 166. 数独

    题目地址 https://www.acwing.com/problem/content/description/168/ 题目描述 数独是一种传统益智游戏,你需要把一个9 × 9的数独补充完整,使得图 ...

  3. JVM的类加载过程以及双亲委派模型详解

    JVM的类加载过程以及双亲委派模型详解 这篇文章主要介绍了JVM的类加载过程以及双亲委派模型详解,类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象 ...

  4. 使用adb安装apk到手机

    [ADB]Android debug bridge.Android手机实际是基于Linux系统的.通过USB线将android手机与电脑连起来,在电脑上dos命令行中敲adb shell命令,可以登录 ...

  5. IT兄弟连 Java语法教程 流程控制语句 控制循环结构3

    使用continue忽略本次循环剩下的语句 continue的功能和break有点类似,区别是continue只是忽略本次循环剩下的语句,接着开始下一次循环,并不会终止循环:而break则是完全终止循 ...

  6. 前端笔记之React(一)初识React&组件&JSX语法

    一.React项目起步配置 官网:https://reactjs.org/ 文档:https://reactjs.org/docs/hello-world.html 中文:http://react.c ...

  7. 01-String(键命令)

    Redis Redis是一个高性能的Key-Value数据库. 学习目标 能够描述出什么是 nosql 能够说出 Redis 的特点 能够根据参考资料修改常用Redis配置 能够写出Redis中str ...

  8. hibernate中的merge()方法

    Hibernate提供有save().persist().savaOrUpdate()和merge()等方法来提供插入数据的功能.前三者理解起来较后者容易一些,而merge()方法从api中的介绍就可 ...

  9. PlayJava Day010

    今日所学: /* 2019.08.19开始学习,此为补档. */ 1.继承补充: ①不要仅为了获取其他类中某个功能而去继承,而是要有所属关系. ②Super关键字: a.代表父类对象的引用,且main ...

  10. 数据库系统原理(第四章:SQL与关系数据库基本操作 )

    一.SQL概述 sql是结构化查询语言(Structured Query Language,SQL)是专门用来与数 据库通信的语言,它可以帮助用户操作关系数据库. SQL的特点: SQL不是某个特定数 ...