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. AcWing 791. 高精度加法 解题记录

    题目地址 https://www.acwing.com/problem/content/description/793/ 题目描述给定两个正整数,计算它们的和. 输入格式共两行,每行包含一个整数. 输 ...

  2. 01. Go 语言简介

    Go语言简介 引用原文地址:http://m.biancheng.net/golang/ Go语言也称 Golang,兼具效率.性能.安全.健壮等特性.这套Go语言教程(Golang教程)通俗易懂,深 ...

  3. LeetCode解题笔记 - 20. Valid Parentheses

    这星期听别人说在做LeetCode,让他分享一题来看看.试了感觉挺有意思,可以培养自己的思路,还能方便的查看优秀的解决方案.准备自己也开始. 解决方案通常有多种多样,我觉得把自己的解决思路记录下来,阶 ...

  4. 使用composer安装Larave提示“Changed current directory to C:/Users/Administrator/AppData/Roaming/Composer”

    解决办法: 根据官方手册执行composer global require "laravel/installer" 显示Changed current directory to C ...

  5. __module__和__class__

    目录 一.__module__ 二.__class__ # lib/aa.py class C: def __init__(self): self.name = 'SB' # index.py fro ...

  6. oracle--共享磁盘挂载

    01,查看挂载的磁盘 [root@SHLPDBWX01 ~]# fdisk -l Disk /dev/sda: bytes heads, sectors/track, cylinders Units ...

  7. python--8大排序(原理+代码)

    常用的排序方法:冒泡排序.选择排序.插入排序.快速排序.堆排序.归并排序 冒泡排序(Bubble Sort): 比较相邻的元素.如果第一个比第二个大(升序),就交换他们两个. 对每一对相邻元素作同样的 ...

  8. HTML连载44-标准排版、浮动排版

    一.什么网页的布局方式? 网页的布局方式其实就是指浏览器是如何对网页的元素进行排版的 二.标准流排版方式(又称为文档流.普通流) 1.含义 其实浏览器默认的排版方式就是标准流排版方式 2.在CSS中将 ...

  9. iOS:捋一遍View的生命周期

    一.介绍 前面介绍了VC的生命周期,闲着没事也来捋一捋View的生命周期,简单用两个类型的View来监测.一个View纯代码创建,另一个View使用Xib创建. 二 .代码 MyCodeView:  ...

  10. IT兄弟连 Java语法教程 数组 什么是数组

    数组是编程语言中最常见的一种数据结构,可用于存储多个数据,每个数组元素存放一个数据,通常可通过数组元素的索引来访问数组元素,包括为数组元素赋值和取出数组元素的值.Java语言的数组则具有其特有的特征, ...