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. vscode 问题。。。。

    "program": "${workspaceFolder}/a.out", "preLaunchTask": "build&qu ...

  2. 乘积量化(Product Quantization)

    乘积量化 1.简介 乘积量化(PQ)算法是和VLAD算法是由法国INRIA实验室一同提出来的,为的是加快图像的检索速度,所以它是一种检索算法,在矢量量化(Vector Quantization,VQ) ...

  3. Python程序中的进程操作-进程间通信(multiprocess.Queue)

    目录 一.进程间通信 二.队列 2.1 概念介绍--multiprocess.Queue 2.1.1 方法介绍 2.1.2 其他方法(了解) 三.代码实例--multiprocess.Queue 3. ...

  4. Rails + Webpacker + Puma + Nginx 部署

    准备 ssh 登录 首先 ssh 登录服务器,免密码登录可以参考 ssh 免密码登录服务器 创建部署用户 $ sudo adduser deploy 安装依赖 Ruby 这里使用 RVM 安装和管理 ...

  5. Java连载46-Java中的多态

    一.多态的语法 1.关于多态中涉及到几个概念 (1)向上转型(upcasting) 子类型转换为父类型,又被称为自动类型转换 (2)向下转型(downcasting) 父类型转换为子类型,又被称为强制 ...

  6. python做中学(七)ord() 函数

    描述 ord() 函数是 chr() 函数(对于8位的ASCII字符串)或 unichr() 函数(对于Unicode对象)的配对函数,它以一个字符(长度为1的字符串)作为参数,返回对应的 ASCII ...

  7. PHP面试常考之设计模式——建造者模式

    建造者模式 介绍 建造者模式又名生成器模式,是一种对象构建模式.它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象. 建造者模式是一步一步创建 ...

  8. Koa 中间件的执行

    Node.js 中请求的处理 讨论 Koa 中间件前,先看原生 Node.js 中是如何创建 server 和处理请求的. node_server.js const http = require(&q ...

  9. 【微信小程序】template模板使用详解

    WXML提供模板(template),可以在模板中定义代码片段,然后在不同的地方调用. 模板的作用域: 模板拥有自己的作用域,只能使用 data 传入的数据以及模板定义文件中定义的 <wxs / ...

  10. hadoop mapreduce求解有序TopN

    利用hadoop的map和reduce排序特性实现对数据排序取TopN条数据. 代码参考:https://github.com/asker124143222/wordcount 1.样本数据,假设是订 ...