《Vue 进阶系列之响应式原理及实现》
https://www.bilibili.com/video/av51444410/?p=5
https://github.com/amandakelake/blog/issues/63
https://mp.weixin.qq.com/s/X3s4ysLfwclEOXIuKzOK2g
Vue 进阶系列之响应式原理及实现
以下文章来源于高级前端进阶 ,作者木
木易杨,资深前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!
(给前端大全加星标,提升前端技能)
转自: 高级前端进阶
什么是响应式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中的getter和setter方法。具体使用方法参考如下链接。
MDN之Object.defineProperty
需要注意的是get和set函数是存取描述符,value和writable函数是数据描述符。描述符必须是这两种形式之一,但二者不能共存,不然会出现异常。
实例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.defineProperty中getter和setter的使用方法之后,通过修改get和set函数就可以实现onAChanged和onStateChanged。
实现:
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类,包含两个方法:depend和notify2、创建一个
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 进阶系列之响应式原理及实现》的更多相关文章
- 简单物联网:外网访问内网路由器下树莓派Flask服务器
最近做一个小东西,大概过程就是想在教室,宿舍控制实验室的一些设备. 已经在树莓上搭了一个轻量的flask服务器,在实验室的路由器下,任何设备都是可以访问的:但是有一些限制条件,比如我想在宿舍控制我种花 ...
- 利用ssh反向代理以及autossh实现从外网连接内网服务器
前言 最近遇到这样一个问题,我在实验室架设了一台服务器,给师弟或者小伙伴练习Linux用,然后平时在实验室这边直接连接是没有问题的,都是内网嘛.但是回到宿舍问题出来了,使用校园网的童鞋还是能连接上,使 ...
- 外网访问内网Docker容器
外网访问内网Docker容器 本地安装了Docker容器,只能在局域网内访问,怎样从外网也能访问本地Docker容器? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Docker容器 ...
- 外网访问内网SpringBoot
外网访问内网SpringBoot 本地安装了SpringBoot,只能在局域网内访问,怎样从外网也能访问本地SpringBoot? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装Java 1 ...
- 外网访问内网Elasticsearch WEB
外网访问内网Elasticsearch WEB 本地安装了Elasticsearch,只能在局域网内访问其WEB,怎样从外网也能访问本地Elasticsearch? 本文将介绍具体的实现步骤. 1. ...
- 怎样从外网访问内网Rails
外网访问内网Rails 本地安装了Rails,只能在局域网内访问,怎样从外网也能访问本地Rails? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Rails 默认安装的Rails端口 ...
- 怎样从外网访问内网Memcached数据库
外网访问内网Memcached数据库 本地安装了Memcached数据库,只能在局域网内访问,怎样从外网也能访问本地Memcached数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装 ...
- 怎样从外网访问内网CouchDB数据库
外网访问内网CouchDB数据库 本地安装了CouchDB数据库,只能在局域网内访问,怎样从外网也能访问本地CouchDB数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Cou ...
- 怎样从外网访问内网DB2数据库
外网访问内网DB2数据库 本地安装了DB2数据库,只能在局域网内访问,怎样从外网也能访问本地DB2数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动DB2数据库 默认安装的DB2 ...
- 怎样从外网访问内网OpenLDAP数据库
外网访问内网OpenLDAP数据库 本地安装了OpenLDAP数据库,只能在局域网内访问,怎样从外网也能访问本地OpenLDAP数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动 ...
随机推荐
- 【西北师大-2108Java】第五次作业成绩汇总
[西北师大-2108Java]第五次作业成绩汇总 作业题目 面向对象程序设计(JAVA) 第7周学习指导及要求 实验目的与要求 (1)掌握四种访问权限修饰符的使用特点: (2)掌握Object类的用途 ...
- AcWing 166. 数独
题目地址 https://www.acwing.com/problem/content/description/168/ 题目描述 数独是一种传统益智游戏,你需要把一个9 × 9的数独补充完整,使得图 ...
- JVM的类加载过程以及双亲委派模型详解
JVM的类加载过程以及双亲委派模型详解 这篇文章主要介绍了JVM的类加载过程以及双亲委派模型详解,类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象 ...
- 使用adb安装apk到手机
[ADB]Android debug bridge.Android手机实际是基于Linux系统的.通过USB线将android手机与电脑连起来,在电脑上dos命令行中敲adb shell命令,可以登录 ...
- IT兄弟连 Java语法教程 流程控制语句 控制循环结构3
使用continue忽略本次循环剩下的语句 continue的功能和break有点类似,区别是continue只是忽略本次循环剩下的语句,接着开始下一次循环,并不会终止循环:而break则是完全终止循 ...
- 前端笔记之React(一)初识React&组件&JSX语法
一.React项目起步配置 官网:https://reactjs.org/ 文档:https://reactjs.org/docs/hello-world.html 中文:http://react.c ...
- 01-String(键命令)
Redis Redis是一个高性能的Key-Value数据库. 学习目标 能够描述出什么是 nosql 能够说出 Redis 的特点 能够根据参考资料修改常用Redis配置 能够写出Redis中str ...
- hibernate中的merge()方法
Hibernate提供有save().persist().savaOrUpdate()和merge()等方法来提供插入数据的功能.前三者理解起来较后者容易一些,而merge()方法从api中的介绍就可 ...
- PlayJava Day010
今日所学: /* 2019.08.19开始学习,此为补档. */ 1.继承补充: ①不要仅为了获取其他类中某个功能而去继承,而是要有所属关系. ②Super关键字: a.代表父类对象的引用,且main ...
- 数据库系统原理(第四章:SQL与关系数据库基本操作 )
一.SQL概述 sql是结构化查询语言(Structured Query Language,SQL)是专门用来与数 据库通信的语言,它可以帮助用户操作关系数据库. SQL的特点: SQL不是某个特定数 ...