vue3 beta
版本已经发布快两个月了,相信你们或多或少都有去了解一些vue3
的新特性,也有一部分人调侃学不动了,在我看来,技术确定是不断更迭的,新的技术出现可以提升生产力,落后的技术确定是要被淘汰的,五年前会JQ一把梭就能找到一份还行的工做,如今只会JQ应该不多公司会要了吧。恰好前两天尤大也发了一篇文章讲述了vue3
的制做历程,有兴趣的同窗能够点击连接前往查看,文章是全英文的,英文不是很好的同窗能够借助翻译插件阅读。好了,废话很少说,本篇的主题是手写vue3的响应式功能。html
在写代码前,不妨来看看如何使用vue3吧,咱们能够先去 github.com/vuejs/vue-n… clone一份代码,使用npm install && npm run dev后,会生成一个packages -> vue -> dist -> vue.global.js文件,这样咱们就可使用vue3了,在vue文件夹新建一个index.html文件。vue
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vue3示例</title> </head> <body> <div id="app"></div> <button id="btn">按钮</button> <script src="./dist/vue.global.js"></script> <script> const { reactive, computed, watchEffect } = Vue; const app = document.querySelector('#app'); const btn = document.querySelector('#btn'); const year = new Date().getFullYear(); let person = reactive({ name: '烟花渲染离别', age: 23 }); let birthYear = computed(() => year - person.age); watchEffect(() => { app.innerHTML = `<div>我叫${person.name},今年${person.age}岁,出生年是${birthYear.value}</div>`; }); btn.addEventListener('click', () => { person.age += 1; }); </script> </body> </html> 复制代码
能够看到,咱们每次点击一次按钮,触发person.age += 1;
,而后watchEffect
自动执行,计算属性也相应更新,如今咱们的目标就很明确了,就是实现reactive
、watchEffect
、computed
方法。react
咱们知道vue3
是基于proxy
来实现响应式的,对proxy
不熟悉的能够去看看阮一峰老师的es6教程:es6.ruanyifeng.com/#docs/proxy reflect
也是es6
新提供的API,具体做用也能够参考阮一峰老师的es6教程:es6.ruanyifeng.com/#docs/refle… ,简单来讲他提供了一个操做对象的新API
,将Object对象属于语言内部的方法放到Reflect
对象上,将老Object方法报错的状况改为返回false
值。 下面咱们来看看具体的代码吧,它对对象的get
、set
、del
操做进行了代理。git
function isObject(target) { return typeof target === 'object' && target !== null; } function reactive() { // 判断是否对象,proxy只对对象进行代理 if (!isObject(target)) { return target; } const baseHandler = { set(target, key, value, receiver) { // receiver:它老是指向原始的读操做所在的那个对象,通常状况下就是 Proxy 实例 trigger(); // 触发视图更新 return Reflect.set(target, key, value, receiver); }, get(target, key, receiver) { return Reflect.get(target, key, value, receiver); }, del(target, key) { return Reflect.deleteProperty(target, key); } }; let observed = new Proxy(target, baseHandler); return observed; } 复制代码
上面的代码看上去好像没啥问题,可是在代理数组的时候,添加、删除数组的元素,除了能监听到数组自己要设置的元素变化,还会监听到数组长度length
属性修改的变化,以下图:es6
因此咱们应该只在新增属性的时候去触发更新,咱们添加hasOwnProperty
判断与老值和新值比较判断,只有修改自身对象的属性或者修改了自身属性而且值不一样的时候才去更新视图。github
set(target, key, value, receiver) { const oldValue = target[key]; if (!target.hasOwnProperty(key) || oldValue !== value) { // 新增属性或者设置属性老值不等于新值 trigger(target, key); // 触发视图更新函数 } return Reflect.set(target, key, value, receiver); } 复制代码
上面咱们只对对象进行了一层代理,若是对象的属性对应的值仍是对象的话,它并无被代理过,此时咱们去操做该对象的时候,就不会触发set
,也就不会更新视图了。以下图:npm
那么咱们应该怎么进行深层次的代理呢?设计模式
咱们观察一下person.hair.push(4)
这个操做,当咱们去取person.hair
的时候,会去调用person
的get
方法,拿到属性hair
的值,那么咱们就能够再它拿到值以后判断是不是对象,再去进行深层次的监听。数组
get(target, key, receiver) { const res = Reflect.get(target, key, receiver); return isObject(res) ? reactive(res) : res; }, 复制代码
代理过的对象再去执行reactive
方法的时候,会去从新设置代理,咱们应该避免这种状况,经过hashmap
缓存代理过的对象,这样在再次代理的时候,判断对象存在hashmap
中,直接返回该结果便可。缓存
let obj = { name: '烟花渲染离别', age: 23, hair: [1,2,3] } let person = reactive(obj); person = reactive(obj); person = reactive(obj); 复制代码
hashmap
缓存代理对象咱们使用WeakMap
缓存代理对象,它是一个弱引用对象,不会致使内存泄露。 es6.ruanyifeng.com/#docs/set-m…
const toProxy = new WeakMap(); // 代理后的对象 const toRaw = new WeakMap(); // 代理前的对象 function reactive(target) { // 判断是否对象,proxy只对对象进行代理 if (!isObject(target)) { return target; } let proxy = toProxy.get(target); // 当前对象在代理表中,直接返回该对象 if (proxy) { return proxy; } if (toRaw.has(target)) { // 当前对象是代理过的对象 return target; } let observed = new Proxy(target, baseHandler); toProxy.set(target, observed); toRaw.set(observed, target); return observed; } let obj = { name: '烟花渲染离别', age: 23, hair: [1,2,3] } let person = reactive(obj); person = reactive(obj); // 再去代理的时候返回的就是从缓存中取到的数据了 复制代码
这样reactive
方法就基本已经实现完了。
咱们先来瞅瞅以前是怎么渲染DOM的。
watchEffect(() => { app.innerHTML = `<div>我叫${person.name},今年${person.age}岁,出生年是${birthYear.value}</div>`; }); 复制代码
在初始化默认执行一次watchEffect
函数后,渲染DOM数据,以后依赖的数据发生变化,会自动再次执行,也就会自动更新咱们的DOM内容了,这就是咱们常说的收集依赖,响应式更新。
那么咱们在哪里进行依赖收集,何时通知依赖更新呢?
proxy
对象的get
方法,这个时候咱们就能够收集依赖了。set
方法,咱们在set
中通知依赖更新。 这实际上是一种设计模式叫作发布订阅。咱们在get
中收集依赖:
get(target, key, receiver) { const res = Reflect.get(target, key, receiver); track(target, key); // 收集依赖,若是目标上的key变化,执行栈中的effect return isObject(res) ? reactive(res) : res; } 复制代码
在set
中通知依赖更新:
set(target, key, value, receiver) { if (target.hasOwnProperty(key)) { trigger(target, key); // 触发更新 } return Reflect.set(target, key, value, receiver); } 复制代码
能够看到咱们在get
中执行了一个track
方法进行收集依赖,在set
中执行trigger
触发更新,这样咱们知道了它的过程后,再来看看怎么实现watchEffect
方法。
咱们传入到watchEffect
方法里的函数就是咱们要收集的依赖,咱们将收集到的依赖用栈保存起来,栈是一种先进后出的数据结构,具体咱们看看下面代码实现:
let effectStack = []; // 存储依赖数据effect function watchEffect(fn, options = {}) { // 建立一个响应式的影响函数,往effectsStack push一个effect函数,执行fn const effect = createReactiveEffect(fn, options); return effect; } function createReactiveEffect(fn) { const effect = function() { if (!effectsStack.includes(effect)) { // 判断栈中是否已经有过该effecy,防止重复添加 try { effectsStack.push(effect); // 将当前的effect推入栈中 return fn(); // 执行fn } finally { effectsStack.pop(effect); // 避免fn执行报错,在finally里执行,将当前effect出栈 } } } effect(); // 默认执行一次 } 复制代码
上面咱们只是收集了fn
存到effectsStack
中,可是咱们还没将fn
和对应的对象属性关联,下面步咱们要实现track
方法,将effect
和对应的属性关联。
let targetsMap = new WeakMap(); function track(target, key) { // 若是taeget中的key发生改变,执行栈中的effect方法 const effect = effectsStack[effectsStack.length - 1]; // 最新的effect,有才建立关联 if (effect) { let depsMap = targetsMap.get(target); if (!depsMap) { // 第一次渲染没有,设置对应的匹配值 targetsMap.set(target, depsMap = new Map()); } let deps = depsMap.get(key); if (!deps) { // 第一次渲染没有,设置对应的匹配值 depsMap.set(key, deps = new Set()); } if (!deps.has(effect)) { deps.add(effect); // 将effect添加到当前的targetsMap对应的target的存放的depsMap里key对应的deps } } } function trigger(target, key, type) { // 触发更新,找到依赖effect let depsMap = targetsMap.get(target); if (depsMap) { let deps = depsMap.get(key); if (deps) { deps.forEach(effect => { effect(); }); } } } 复制代码
targetsMap
的数据结构较为复杂,它是一个WeakMap
对象,targetsMap
的key
就是咱们target
对象,在targetsMap
中该target
对应的值是一个Map
对象,该Map
对象的key
是target
对象的属性,Map
对象对应的key
的值是一个Set
数据结构,存放了当前该target.key
对应的effect
依赖。看下面的代码可能会比较清晰点:
let person = reactive({ name: '烟花渲染离别', }); targetsMap = { person: { 'name': [effect] } } // { // target: { // key: [dep1, dep2] // } // } 复制代码
watchEffect
方法,将fn
也就是effect
push到effectStack
栈中,执行fn
,若是fn
中有用到reactive
代理过的对象,此时会触发该代理对象的get
方法,而咱们在get
方法中使用了track
方法收集依赖,track
方法首先从effectStack
中取出最后一个effect
,也就是咱们刚刚push到栈中的effect
,而后判断它是否存在,若是存在的话,咱们从targetMap
取出对应的target
的depsMap
,若是depsMap
不存在,咱们手动将当前的target
做为key
,depsMap = new Map()
做为值设置到targetMap
中,而后咱们再从depsMap
中取出当前代理对象key
对应的依赖deps
,若是不存在则存放一个新Set
进去,而后将对应的effect
添加到该deps
中。set
方法,执行trigger
方法,经过传入的target
在targetsMap
中找到depsMap
,经过key
在depsMap
中找到对应的deps
,循环执行里面保存的effect
。写computed
以前咱们也来回顾下它的用法:
let person = reactive({ name: '烟花渲染离别', age: 23 }); let birthYear = computed(() => 2020 - person.age); person.age += 1; 复制代码
能够看到computed
接受一个函数,而后返回一个通过处理后的值,在依赖的数据发生了修改后,computed
也会从新计算一次。
实际computed
它也是一个watchEffect
函数,不过它比较特殊,这里在调用watchEffect
时候传入了两个参数,一个是computed
的fn
,还有一个就是咱们要给watchEffect
的参数{ lazy: true, computed: true }
,咱们以前写watchEffect
的时候并无对这些参数进行处理,因此如今咱们还得进行处理。
function computed(fn) { let computedValue; const computedEffect = watchEffect(fn, { lazy: true, computed: true }); return { effect: computedEffect, get value() { computedValue = computedEffect(); trackChildRun(computedEffect); return computedValue; } } } function trackChildRun(childEffect) { if (!effectsStack.length) return; const effect = effectsStack[effectsStack.length - 1]; for (let i = 0; i < childEffect.deps.length; i++) { const dep = childEffect.deps[i]; if (!dep.has(effect)) { dep.add(effect); effect.deps.push(dep); } } } 复制代码
修改watchEffect
方法,接收一个opstion
参数而且添加lazy
属性判断,当lazy
为true
时不当即执行传入的函数,由于computed
方法是不会当即执行的。
function watchEffect(fn, options = {}) { // 建立一个响应式的影响函数,往effectsStack push一个effect函数,执行fn const effect = createReactiveEffect(fn, options); // start: 添加的代码 if (!options.lazy) { effect() } // end: 添加的代码 return effect; } 复制代码
修改createReactiveEffect
方法,添加options
参数,而且给当前的effect
添加deps
用于收集被计算的属性的依赖,在本文的实例中就是age
属性的依赖集合,保存computed
、lazy
属性。
function createReactiveEffect(fn, options) { const effect = function() { // 判断栈中是否已经有过该effect,避免递归循环重复添加,好比在监听函数中修改依赖数据 if (!effectsStack.includes(effect)) { try { effectsStack.push(effect); // 将当前的effect推入栈中 return fn(); // 执行fn } finally { effectsStack.pop(effect); // 避免fn执行报错,在finally里执行,将当前effect出栈 } } } // start: 添加的代码 effect.deps = []; effect.computed = options.computed; effect.lazy = options.lazy; // end: 添加的代码 return effect; } 复制代码
在track
方法将收集到的属性依赖集合添加到effect
的deps
。
function track(target, key) { // 若是taeget中的key发生改变,执行栈中的effect方法 const effect = effectsStack[effectsStack.length - 1]; // 最新的effect,有才建立关联 if (effect) { let depsMap = targetsMap.get(target); if (!depsMap) { // 第一次渲染没有,设置对应的匹配值 targetsMap.set(target, depsMap = new Map()); } let deps = depsMap.get(key); if (!deps) { // 第一次渲染没有,设置对应的匹配值 depsMap.set(key, deps = new Set()); } if (!deps.has(effect)) { deps.add(effect); // start: 添加的代码 effect.deps.push(deps); // 将属性的依赖集合挂载到effect // end: 添加的代码 } } } 复制代码
在trigger
方法经过以前保存在effect
的computed
属性区分是computed
函数仍是普通的函数,而后分别保存起来,而后先执行普通的effect
函数,在执行computed
函数。
function trigger(target, key, type) { // 触发更新,找到依赖effect let depsMap = targetsMap.get(target); if (depsMap) { let effects = new Set(); let computedRunners = new Set(); let deps = depsMap.get(key); if (deps) { deps.forEach(effect => { if (effect.computed) { computedRunners.add(effect); } else { effects.add(effect); } }); } if ((type === 'ADD' || type === 'DELETE') && Array.isArray(target)) { const iterationKey = 'length'; const deps = depsMap.get(iterationKey); if (deps) { deps.forEach(effect => { effects.add(effect); }); } } computedRunners.forEach(computed => computed()); effects.forEach(effect => effect()); } } 复制代码
computed
执行流程咱们来根据下面的代码来分析执行流程。
const value = reactive({ count: 0 }); const cValue = computed(() => value.count + 1); let dummy; watchEffect(() => { dummy = cValue.value; console.log(dummy) }); value.count = 1; 复制代码
第一步:先将count
对象转换成响应式的对象。
第二步:执行computed
方法,computed
内部会执行watchEffect
,而且传入lazy
、computed
属性,因为传入了lazy
为true
,因此并不会当即执行生成的effect
,为了区分,下面统称这个effect
为计算effect
,将传入的fn
称为计算fn
,也就是不会往栈中添加数据,此时cValue
保存的是一个包含计算effect
和get
方法的对象。
第三步:执行watchEffect
方法:这也是最关键的一步,执行watchEffect
方法,因为没有带lazy
属性,因此此时会马上执行effect
方法,往effectsStack
中添加当前的effect
,而后执行fn
。
第四步:执行fn
,执行fn
中会去获取cValue
的值,此时触发了computed
的get
方法,而后执行第二步保存的计算effect
。
第五步:执行计算effect
,将计算effect
添加到effectsStack
中(此时的effectsStack
为[普通effect, 计算effect]
),而后执行计算fn
。
第五步:执行计算fn
,计算fn
依赖了响应式对象value
,此时读取value
的count
属性,触发value
对象的get
方法,get
方法中执行track
方法收集依赖。
第六步:执行track
方法,拿到栈中最后一个元素也就是计算effect
,初始化targetsMap
和depsMap
,而后将计算effect
保存到count
对应的deps
中,同时也将deps
保存到计算effect
的deps
中,下一步要用,这样就造成了一个双向收集的关系,计算effect
保存了count
的全部依赖,count
也存了计算effect
的依赖,track
方法执行完执行下一步,返回获取到的value.count
的值,存到computedValue
中,而后咱们继续往下执行。
第六步:执行trackChildRun
,计算fn
执行完则将计算effect
从栈中推出,此时effectsStack
的栈顶为普通effect
,首先咱们在trackChildRun
中拿到栈尾元素也就是剩下的普通effect
,而后循环传入的计算effect
的deps
数据,咱们在上一步执行track
的时候,在计算effect
的deps
中保存了count
属性对应的依赖集合,此时的deps
中只有一个元素[计算effect]
,如今将普通effect
也添加到dep
中,因此此时depsMap
为{ count: [计算effect, 普通effect] }
。
第七步:执行value.count = 1
,触发set
方法,执行trigger
方法,获取到count
对应的deps
也就是[计算effect, 普通effect]
,循环deps
分别存储普通的effect
和计算effect
,而后前后执行计算effect
和普通effect
。
感谢小伙伴们看到了这里,以为本文写的不错的点个赞再走呗。(^o^)/~