vue源码限于水平,看的乱七八糟,只是看了一些文章解析有些收获,遂记。
现在vue3已经出来了,是不是研究vue2.x过时了?非也非也。
抛去大部分vue使用者还在用的情况,vue3的很多思路都可以在vue2中得到体现,所以学习还是有必要的。
这篇文章就聊聊到响应式原理,从百草园到三味书屋,哦不,从 Vue2 到 Vue3。 聊Vue2响应式就必须聊聊 Objet.defineProperty⇲...
talk is cheap.....
Objet.defineProperty
用过 vue 的都知道,vue 如果有个数据 firstName 改变了,那么相应用到 firstName 的地方都得改变,
比如
计算属性(computed)
computed: {
fullName: () => this.firstName + this.lastName
}
数据监听(watch)
watch: {
firstName: function (val, oldVal) {
// dosomething
}
}
模板渲染(render)
<span>姓: {{ firstName }}<span>
这三个地方就相当于 firstName 的依赖(Watcher),后面细讲。
另外 Vue 中数据定义是这样的
new Vue({
el: '#app',
data: { firstName: '王' }
})
那么怎么才能监听到 fisrtName 的改变并更新相应的数据呢?
在 Object.defineProperty 之前, 是 MutationObserver -MDN ⇲,他的作用如下
MutationObserver接口提供了监视对DOM树所做更改的能力。它被设计为旧的Mutation Events功能的替代品
这是已经淘汰的东西,不多做介绍, vue2.x 做法就是利用 Objet.defineProperty 监听 data 的改变从而去触发各个部分的更新。
做个简单的介绍,可以猛戳 这里⇲ 了解更多. 以上面为例
const data = { firstName: '李' };
const vm = {};
Object.defineProperty(vm, "firstName", {
enumerable: true,
configurable: true,
get() { console.log('我被获取了!'); return data.firstName; },
set(newValue) {
data.firstName = newValue;
console.log('我被改变了, 这是新值:', newValue)
},
});
vm.firstName = '李';
console.log(vm.firstName)
// 我被改变了, 这是新值: 李
// 我被获取了!
// 李
上面给 vm 定义一个 响应式 firstName 属性,并且这个属性可枚举(enumerable : true)、可配置(configurable : true)。firstName 改变就就会触发 set 方法,获取 firstName 就会触发 get 方法。
把响应式核心 实现方法说了,再来理一理 vue 响应式实现原理。
vue响应式原理
先上一张图,来自 掘金小册(推荐!) 剖析 Vue.js 内部运行机制⇲
上面的图基本就很经典的概括了 Vue 的生命周期。包含 初始化、编译、挂载、更新 。。。而Vue的响应式也基本贯穿了整个周期。
另外注意:
- 上图
watcher不是代码中的 vue 中的 watch ,而是依赖。 - watcher(依赖) 有三类:
computed watcher、watch watcher、render watcher,这里提一嘴,后面再说。
关于响应式原理这张图概括的比较全了👍(看完手写,建议回头再来看看这张图)。但凡看着好的我都采用拿来主义🤪
图来自 图解 Vue 响应式原理⇲
手写一个vue2.x响应式
本节主要
参考(摘抄)😅 手写一个简易vue响应式带你了解响应式原理 ⇲效果 如下(v-model, v-show, v-text {{}})
原版实现
首先回忆一下 Vue 的使用
new Vue({
el: '#app',
data: {
name: 'ethan',
text: 'text',
}
})
让我们由 Vue 类切入
Vue
首先需要定义一个 Vue 类, 需要完成以下几点功能:
- 将 data 变为响应式。
- 能编译模板,识别其中绑定的数据。
对于第一点 利用 Observer 将数据变为响应式,另外为了 在模板 template 中 使用 {{ firstName }} 而不是 {{ data.firsName }}, 那么需要将 data 的属性映射到 Vue 中方便直接调用,利用 _proxyData 方法实现。
对于第二点 专门写个 Compiler 实现。
class Vue {
constructor(options) {
this.$options = options || {}
// 传ID或者Dom都可以。
this.$el = typeof options.el === 'string' ?
document.querySelector(options.el) : options.el;
this.$data = options.data;
// 处理data中的属性
this._proxyData(this.$data);
// 将data变为响应式
new Observer(this.$data)
// 模板编译
new Compiler(this)
}
// 将data中的属性注册到vue
_proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get(){ return data[key] },
set (newValue) {
if(newValue === data[key]) return;
data[key] = newValue;
}
})
})
}
}
Observer
再来实现 Observer, 它作用如下:
- 将对象数据每个属性及其子属性变为响应式,
对于这一点, 遍历每个属性将其变为响应式,利用 方法 walk 实现.对于单个属性的响应式,利用 Object.defineProperty 处理, 封装 为 defineReactive 方法。
注意:
- 利用 walk 递归,使data的每一个属性都具有响应性,包含新更改的值
- 每次在获取的时候添加依赖.
- 每次数据更新时通知
Dep通知(notify)更新
于是有下面代码
/*
将数据变为响应式对象
*/
class Observer {
constructor(data) {
this.walk(data);
}
walk(data) {
if(!data || typeof data != 'object') return;
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
})
}
defineReactive(obj, key, value) {
// 递归的将对象子属性变为响应式
this.walk(value);
const self= this;
let dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
//这里 target 即为 Watcher, 存在即添加依赖
Dep.target && dep.addSub(Dep.target)
// 注意,这里返回 Obj[key] 会爆栈!!(死循环)
return value
},
set (newValue) {
// 旧值与新值相同则不更新
if(newValue === obj[key]) return;
value = newValue;
// 如果新值是
self.walk(newValue)
// 更新视图
dep.notify()
}
})
}
}
Compiler
接下来 是 模板编译 Compiler 的实现,作用如下
- 解析
Dom中绑定的数据 - 解析
Dom中的指令(v-model,v-text等)
实现上面两点,只需要 把 dom 当成一个字符串 利用正则解析即可。
注意
- 文本节点,元素节点有不同的解析方法,针对性处理
- 对于指令,通过获取 js 获取 Dom 自定义属性即可解决
- 对于input节点需要监听改变。利用
change事件
其实除了文本节点、元素节点还有很多节点需要针对性处理,想要了解更多,建议查看 vue-design---渲染器之挂载⇲
下面是 Compiler 的实现
//解析模板template 内容,变成dom树
class Compiler {
constructor(vm) {
this.vm = vm;
this.el = vm.$el;
this.compile(this.el)
}
compile(el) {
let childrenNodes = [...el.childNodes]
// 遍历每一个节点 针对性的解析
childrenNodes.forEach(node => {
if(this.isTextNode(node)){
this.compileText(node)
}else if(this.isElementNode(node)) {
this.compileElement(node)
}
if(node.childNodes && node.childNodes.length) this.compile(node)
})
}
// 文本节点编译
compileText(node){
let reg = /\{\{(.+?)\}\}/
let val = node.textContent
if(reg.test(val)){
let key = RegExp.$1.trim()
const value = this.vm[key];
node.textContent = val.replace(reg, value)
new Watcher(this.vm, key, (newVal) => {
node.textContent = newVal
})
}
}
// 元素节点编译
compileElement(node) {
// https://developer.mozilla.org/zh-CN/docs/Web/API/Element/attributes
![...node.attributes].forEach(attr => {
let attrName = attr.name
if(this.isDirective(attrName)){
attrName = attrName.substring(2);
let key = attr.value;
this.update(node, key, attrName)
}
})
}
// 动态运行不同的更新方法
update(node, key, attrName) {
let updateFn = this[attrName+'Update']
updateFn && updateFn.call(this, node, key, this.vm[key])
}
// 文本节点的值有更新就会重新渲染。本质还是利用 js修改 textContent
textUpdate(node, key, content ){
node.textContent = content
new Watcher(this.vm, key, newVal => { node.textContent = newVal })
}
// v-model 更新
modelUpdate(node, key, value) {
const typeAttr = node.getAttribute('type')
// 对于输入框,做了过滤
if(typeAttr == "text") {
node.value = value;
new Watcher(this.vm, key, newVal => { node.value = newVal})
node.addEventListener('keyup', () => {
this.vm.$data[key] = node.value
})
}
}
isDirective(attr) {
return attr.startsWith('v-')
}
isTextNode(node){
return node.nodeType === 3
}
isElementNode(node) {
return node.nodeType === 1
}
}
可以看到,上面三处添加了 Watcher (Watcher 的实现后面会讲)
- 文本节点更新时(
compileText), 如<span>{{ name }}</span> - 自定义指令 v-text 更新时(
textUpdate), 如<span v-text="name"></span> - 自定义指令 v-modal 更新时(
modelUpdate), 如<input v-modal="name"></input>
因为在上述三种情况会产生name的依赖(前面提到的 三种Watcher 中的一种 render Watcher)。
Dep
再来说说 Dep 的实现,主要作用
- 收集依赖 和 更新依赖
主要是暴露方法 addSub 和 notify 。请看下面👇
// 收集依赖 和 通知依赖更新
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
Watcher
最后说说 Watcher, 文中说的最多的 依赖 就是它!作用是什么呢?
- 更新 Dep 的 target(值为 Watcher 本身)
- 利用 回调函数
cb触发更新
代码如下
class Watcher {
constructor(vm, key, cb){
this.vm = vm
this.key = key
this.cb = cb
Dep.target = this
this.oldVal = vm[key]
Dep.target = null
}
update(){
let newValue = this.vm.$data[this.key]
if(newValue === this.oldVal) return;
// 调用回调更新
this.cb(newValue)
}
}
还记得吗 ? 在 前面 Observer 定义响应时有这一段代码👇
class Observer{
//...
get(){
Dep.target && dep.addSub(Dep.target)
return value
},
}
每次在实例化 Watcher 的时候就为 Dep 添加 target, 每次执行 data[prop] 的时候 就会进行依赖收集!
好了,终于都写的差不多了。测试一下看看?
测试一下
麻雀虽小,五脏俱全了,不过他们的执行顺序需要做些调整:
<body>
<div id="app">
<input type="text" v-model="name" /><br/>
<b>姓名:</b><span>{{ name }}</span> <br/>
<b>性别:</b><span>{{ sex }}</span>
<hr />
<div v-text="text"></div>
</div>
<script>
// 依赖收集和更新
class Dep { //... }
// 依赖,暴露更新方法 notify
class Watcher { //... }
// 解析模板 Dom 中的依赖
class Compiler { //... }
// 将Data变为响应式
class Observer { //.. }
// Vue 实例
class Vue { //.. }
new Vue({
el: '#app',
data: {
name: 'ethan',
sex: '男',
text: 'text',
}
})
</script>
</body>
当然,
v-text 也是双向绑定,主要是 compileText 的作用。改成 name 试一下
<body>
<div id="app">
<input type="text" v-model="name" /><br/>
<b>姓名:</b><span>{{ name }}</span> <br/>
<b>性别:</b><span>{{ sex }}</span>
<hr />
<div v-text="name"></div>
</div>
<script>
//...
</script>
</body>
好像可以哎~~~,哇哦🤸♂️🤸♂️🤸♂️,不得不说我抄写代码有点🤏东西,家人们,点个赞吧🤡 什么?不给?没事😁请继续看下去。
修复原版bug和扩展v-show
Bug发现
上面实现了,我就有个想法,性别通过选择更改可以不可以呢🤷♂️?
说干就干,首先就要监听选择更改的值,然后将 data.sex 赋值为新值不就好了。
当时心想怪简单的,结果我折腾了好一会🤣,都忘完了。
html
<div id="app">
<input type="text" v-model="name" /><br/>
<input id="male" name="sex" type="radio" v-model="sex" value="男">
<label for="male"> 男 </label>
</input>
<input id="female" name="sex" type="radio" v-model="sex" value="女">
<label for="female"> 女 </label>
</input><br/>
<b>姓名:</b><span>{{ name }}</span> <br/>
<b>性别:</b><span>{{ sex }}</span>
<hr />
<div v-text="text"></div>
</div>
js
//...
class Compiler {
constructor(vm) {}
compile(el) {}
compileText(node){}
compileElement(node) {}
update(node, key, attrName) {
let updateFn = this[attrName+'Update']
updateFn && updateFn.call(this, node, key, this.vm[key])
}
// 文本节点的值有更新就会重新渲染。本质还是利用 js修改 textContent
textUpdate(node, key, content ){}
modelUpdate(node, key, value) {
const typeAttr = node.getAttribute('type')
if(typeAttr == "text") {
}
// 增加这里
else if(typeAttr === "radio") {
// 可以不用 Wather 也可以 用个Watcher 去更改类名, 在下一小节展示
const nameAttr = node.getAttribute('name')
// 这里不需要 watch,
node.addEventListener('change', (ev) => {
this.vm.$data[key] = ev.target.value
})
}
}
isDirective(attr) {}
isTextNode(node){}
isElementNode(node) {}
}
可以看到,第一次切换 "女" 时更新,后面的切换都不更新了?为啥,各位同志们可以先想想💭。
不卖关子了,原因就是 旧值 this.oldVal 是在 new Watcher 时确定,后面在更新"男"=> "女" 时,自然更新 ;当再选择 "男" ,上次选择 "女" 时Watcher 中的 this.oldVal 并没有更新,所以值相同不会触发 Watcher,具体代码在 Watcher 的 update 方法内体现。
当再选择 "女" 虽然触发了 Watcher, 但是看起来也没有改变(原来的值就为 "女")
找到原因就好改了。在 notify 中将旧值传入,在 Watcher 的 update 中接收并更新 this.oldVal,具体就是
class Dep {
//...
notify(oldValue){
// 接收旧值↑ 并传递↓
this.subs.forEach(sub => {
sub.update(oldValue)
})
}
}
class Watcher {
//...
update(oldValue){
// 接收↑ 并更新旧值↓
this.oldVal = oldValue;
let newValue = this.vm.$data[this.key]
if(newValue === this.oldVal) return;
this.cb(newValue)
}
}
class Observer {
//...
defineReactive(obj, key, value) {
//...
Object.defineProperty(obj, key, {
//...
set (newValue) {
// 拿旧值
const oldval = obj[key]
if(newValue === obj[key]) return;
value = newValue;
self.walk(newValue)
// 更新视图,并传递旧值
dep.notify(oldval)
}
})
}
}
这样就可以了。
扩展v-show
有了前面的实现, v-show 实现起来就简单了,主要监听输入,更新值即可。
css
<style>
.show-txt {
opacity: 1;/* 为了观赏性,不用display */
transition: all .5s linear;
}
.show-txt.hidden {
opacity: 0;
color: #eee;
}
</style>
html
<div id="app">
<input type="text" v-model="name" /><br/>
<input id="male" name="sex" type="radio" v-model="sex" value="男">
<label for="male"> 男 </label>
</input>
<input id="female" name="sex" type="radio" v-model="sex" value="女">
<label for="female"> 女 </label>
</input><br/>
<input name="show" type="checkbox" v-model="show" checked>是否展示</input>
<div class="show-txt" v-show="show">展示文本示例</div><br/>
<b>姓名:</b><span>{{ name }}</span> <br/>
<b>性别:</b><span>{{ sex }}</span>
<hr />
<div v-text="text"></div>
</div>
js
//...
class Compiler {
//..
modelUpdate(node, key, value) {
const typeAttr = node.getAttribute('type')
if(typeAttr == "text") {}
else if(typeAttr === "radio") {}
else if(typeAttr === 'checkbox') { // 监听值
node.addEventListener('change', (ev) => {
this.vm.$data[key] = ev.target.checked
})
}
}
// v-show 指令绑定值更新的回调
showUpdate(node, key, value){
const change = (val) => {
const operate = !!val ? 'remove' : 'add';
node.classList[operate]('hidden')
}
change(value);
new Watcher(this.vm, key, (newVal) => { change(newVal) })
}
// ...
}
//...
// 测试
new Vue({
el: '#app',
data: {
name: 'ethan',
sex: '男',
text: 'text',
show: true,
}
})
好了目前就实现到到这里了,Vue2的响应式原理手写实现告一段落。如果你更倾向源码去寻找答案 推荐 Vue 源码解读(3)—— 响应式原理 ⇲
当然上面只是对象属性的更新,Vue源码对数组的更新检测利用重写原型的方法。下面稍作介绍。
数组检测
以下八种数组方法,vue是可以检测其改变的,
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
其做法就是在数组原型拦截这些方法,可从源码窥其一二。
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// 缓存原始方法
var original = arrayProto[method];
def(arrayMethods, method, function mutator () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
var result = original.apply(this, args);
// 拿到全局的观察者,相当于上面的 Observer
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
break
}
// 存在插入,这监听这两个属性
if (inserted) { ob.observeArray(inserted); }
// 通知更新
ob.dep.notify();
return result
});
});
扩展:Vue3的响应式实现
Vue3对Vue2来说大大的升级了,本文只探究响应式原理,这里大概总结一下。
Vue3响应式原理
先来个对比。
Vue2.x
- 基于
Object.defineProperty,不具备监听数组的能力,需要重新定义数组的原型来达到响应式。 Object.defineProperty无法检测到对象属性的添加和删除 。- 由于Vue会在初始化实例时对属性执行
getter/setter转化,所有属性必须在 data 对象上存在才能让 Vue 将它转换为响应式。 - 深度监听需要一次性递归,对性能影响比较大。
Vue3
- 基于
Proxy和Reflect,可以原生监听数组,可以监听对象属性的添加和删除。 - 不需要一次性遍历 data 的属性,可以显著提高性能。
- 因为 Proxy 是ES6新增的属性,有些浏览器还不支持,只能兼容到IE11 。
对于Vue3的响应式,网上⇲ 找到一张图,总结比较贴切。
关于 Proxy
相比
Object.defineProperty,Proxy⇲ 支持的对象操作十分全面:get、set、has、deleteProperty、ownKeys、defineProperty......等等
掘金不少讲的好的文章 🏸- proxy,不再赘述,给一个例子,可以带着好奇去对 Proxy 扫盲。
let data = [1, 2, 3]
let p = new Proxy(data, {
get(target, key, receiver) {
console.log('get value:', key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('set value:', key, value)
Reflect.set(target, key, value, receiver)
return true // 在严格模式下,若set方法返回false,则会抛出一个 TypeError 异常。
}
})
p.length = 4 // set value: length 4
console.log(data) // [1, 2, 3, empty]
p.shift()
// get value: shift
// get value: length
// get value: 0
// get value: 1
// set value: 0 2
// get value: 2
// set value: 1 3
// set value: length 3
p.push(4)
// get value: push
// get value: length
// set value: 3 4
// set value: length 4
p
// Proxy {0: 2, 1: 3, 3: 4}
p[3] = 0
// set value: 3 0
手写响应式
我要开始动手了!什么?动手?不是不是,亲爱读者们,我是说我要动手开代码了.
主要的函数如下:
仅涉及 Composition API 响应式原理实现,没有涉及到编译部分(Dom部分)
/* 依赖收集:建立 数据&cb 映射关系 */
function track(target, key){}
/* 触发更新:根据映射关系,执行cb */
function trigger(target, key){}
/* 建立响应式数据 */
function reactive(obj){}
/* 声明响应函数cb(依赖响应式数据) */
function effect(cb){}
限于篇幅且代码也不多,这里就直接给代码了,请参考注释阅读。
代码来自(建议精读): 林三心画了8张图,最通俗易懂的Vue3响应式核心原理解析⇲
const targetMap = new WeakMap()
// 收集依赖(effect)
function track(target, key) {
// 如果此时activeEffect为null则不执行下面
// 这里判断是为了避免例如console.log(person.name)而触发track
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}
dep.add(activeEffect) // 把此时的activeEffect添加进去
}
// 触发更新
function trigger(target, key) {
let depsMap = targetMap.get(target)
if (depsMap) {
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
}
// 定义响应式,劫持数据
function reactive(target) {
const handler = {
// reciver 为 Proxy 对象
get(target, key, receiver) {
track(receiver, key) // 访问时收集依赖
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
trigger(receiver, key) // 设值时自动通知更新
}
}
return new Proxy(target, handler)
}
let activeEffect = null
// 依赖存储,其实也可以用个栈来存依赖,下面 的实现方式 是不是多少// 有点熟悉,没错 vue2 响应式实现时是这样干的
function effect(fn) {
activeEffect = fn
activeEffect()
activeEffect = null
}
上面代码如果理解比较吃力,建议查看上面提到 原文 👆以及文章 Vue3响应式原理 + 手写reactive⇲(看完记得回来,我后面有一些低质量扩展给到大家🤡),讲的比较细。
主要用了 Proxy 劫持数据(替代 Object.defineProperty), 其他方面比如依赖收集、触发更新实现和 Vue2 基本一样。
测试一下
const data = { name: '二蛋' }
const rData = reactive(data)
// 有一个 data.name 的依赖
effect(() => {
console.log('我依赖二蛋', rData.name);
})
rData.name = '王二蛋'
// 我依赖二蛋 二蛋
// 我依赖二蛋 王二蛋
除了上面的 reactive,还有个 ref, 是针对单个变量响应式设计的。另外 reactive 实现了,ref 其实是 基于 reactive 的扩展, 仅 属性 value 为响应式!
- ref
function ref(initValue) {
return reactive({
value: initValue
})
}
基于上面代码,其它的响应式 API⇲ computed 、 watchEffect 、 watch也可以实现了。
- watchEffect
function watchEffect(fn) {
effect(() => fn())
}
const eData = ref(5);
watchEffect(() => { console.log('effect测试: ', eData.value) })
eData.value = 666
}
// effect测试: 5
// effect测试: 666
- Computed
function computed(fn) {
const result = ref()
effect(() => result.value = fn())
return result
}
const ref1 = ref(5);
const cRef1 = computed(() => {
console.log('computed测试: ', ref1);
return ref1.value
});
ref1.value = 666;
console.log('last: ', ref1, cRef1)
// computed测试: Proxy {value: 5}
// computed测试: Proxy {value: 666}
- watch
支持监听单个值和多个值,并且新旧值都要回传,另外要注意的是 监听多个源时 某一个 更新,要触发回调 fn 从而精确更新(Map实现)。当然前面的代码要稍作修改,具体如下:
function track(target, key) {
if (!activeEffect.fn) return
//...
dep.add(activeEffect.fn) // 原来的回调我放在了 fn 上
// 此时的 dep 才是后面更新的 dep 将其当做更新的 key
activeEffect.key = dep;
}
function trigger(target, key, { oldValue }) {
//...
// 旧值和dep(作为key)一起传递
dep.forEach(effect => effect(oldValue, dep))
}
function reactive(target) {
const handler = {
//...
set(target, key, value, receiver) {
// 获取旧值以返回
const oldValue = Reflect.get(target, key, receiver)
if(value === oldValue) return;
const result = Reflect.set(target, key, value, receiver)
trigger(receiver, key, { newValue: value, oldValue}) // 设值时自动通知更新
return result
}
}
return new Proxy(target, handler)
}
let activeEffect = {fn: null, key: null}
function effect(fn) {
activeEffect.fn = fn
activeEffect.fn()
activeEffect = {fn: null, key: null}
}
function watch(source, fn) {
let oldValues = new Map(), newValues = new Map(), isArray = false;
function handleEffects(rValue){
effect((oldValue, upKey = null) => {
// 这里去触发get 搜集依赖
const newValue = typeof rValue === 'function' ? rValue().value : rValue.value;
if(activeEffect.fn) {
// 遍历原始数据时,新值旧值都一样, 添加依赖的时候 增加 activeEffect.key
oldValues.set(activeEffect.key, newValue)
newValues.set(activeEffect.key, newValue)
}else {// 同时更新旧值与新值
oldValues.set(upKey, oldValue)
newValues.set(upKey, newValue)
// 同源和多源做不同处理
isArray
? fn([[...newValues.values()], [...oldValues.values()]])
: fn([...newValues.values()][0], [...oldValues.values()][0]);
}
})
}
// 监听多源
if(Array.isArray(source)) {
isArray = true;
source.forEach(rValue => {
handleEffects(rValue)
})
}
// 监听单一源
else
handleEffects(source)
}
测试一下
const wRef = ref(5);
watch(wRef, (value, preValue) => {
console.log('watch监听单源测试:', value, preValue)
})
wRef.value = 66
// watch监听单源测试: 666 5
const wRef = ref(1);
const wRef1 = ref(2);
const wRef2 = ref(3);
watch([wRef, () => wRef1, wRef2], (values, preValues) => {
console.log('watch监听多源测试:', values, preValues)
})
wRef.value = 11;
// watch监听多源测试:[11, 2, 3] [1, 2, 3]
wRef1.value = 22;
// watch监听多源测试:[11, 22, 3] [1, 2, 3]
wRef2.value = 33
// watch监听多源测试:[111, 22, 33] [11, 2, 3]
wRef.value = 111
// watch监听多源测试:[11, 22, 33] [1, 2, 3]
wRef2.value = 333
// watch监听多源测试:[111, 22, 33] [11, 2, 33]
简版 watch 基本就实现了,仅支持 ref, reactive 的数据会有问题,各位可以想想哪里有问题。
总结
vue2 和vue3 响应基本思路都差不多。收集依赖,数据源改变时更新依赖。
写完感觉自己越来越无知,吾生也有涯,而知也无涯。
另外看起来可能好理解,建议还是动动手吧。
纸上得来终觉浅,绝知此事要躬行!
限于技术和写文章水平,行文有误欢迎👐大家指出。
参考
站在别人肩膀上才能看的更远,感谢 💖💖💖 文章中所有参考链接文章作者的分享。
🔗 | VUE源码相关面试题汇总
🔗 | Vue 3 响应式原理及实现
🔗 | 浅析Vue3中的响应式原理