
作者:大转转FE
转发链接:https://mp.weixin.qq.com/s/Jb5NKXNhlb1Enq0R1p9DcA
看完上两章 初入茅庐 小试牛刀 之后,大家应该对vue-next(Vue 3.0) 的 API 使用已经了如指掌了。好奇的同学一定对 vue-next 响应式的原理充满好奇,本章就带你解密!
带你了解 vue-next(Vue 3.0)之 初入茅庐【实践】
带你了解 vue-next(Vue 3.0)之 小试牛刀【实践】
前言
最新 vue-next 的源码发布了,虽然是 pre-alpha 版本,但这时候其实是阅读源码的比较好的时机。在 vue 中,比较重要的东西当然要数它的响应式系统,在之前的版本中,已经有若干篇文章对它的响应式原理和实现进行了介绍,这里就不赘述了。在 vue-next 中,其实现原理和之前还是相同的,即通过观察者模式和数据劫持,只不过对其实现方式进行了改变。
因此,这篇文章我也打算按这种风格来写一下利用最近空闲时间阅读 vue-next 响应式模块的源码的一些心得与体会,算是抛砖引玉,同时实现一个极简的响应式系统。
如有错误,还望指正。
vue-next 数据响应机制 - Proxy
在学习 vue-next之前,你必须要先熟练掌握ES6中的 Proxy、 Reflect 及 ES6中为我们提供的 Map、 Set两种数据结构
先应用再说原理:
const { reactive, effect} = Vuelet p = reactive({name:'zhuanzhuan'});// effect方法会立即被触发effect(()=>{ console.log(p.name);})p.name = '转转'; // 修改属性后会再次触发effect方法
源码是采用 ts编写,为了便于大家理解原理,这里我们采用js来从0编写,之后再看源码就非常的轻松啦!
reactive方法实现
看源码
function reactive(target) { // if trying to observe a readonly proxy, return the readonly version. if (readonlyToRaw.has(target)) { return target; } // target is explicitly marked as readonly by user if (readonlyValues.has(target)) { return readonly(target); } return createReactiveObject( target, rawToReactive, reactiveToRaw, mutableHandlers, mutableCollectionHandlers ); }function createReactiveObject( target, toProxy, toRaw, baseHandlers, collectionHandlers ) { if (!isObject(target)) { { console.warn(`value cannot be made reactive: ${String(target)}`); } return target; } // target already has corresponding Proxy let observed = toProxy.get(target); if (observed !== void 0) { return observed; } // target is already a Proxy if (toRaw.has(target)) { return target; } // only a whitelist of value types can be observed. if (!canObserve(target)) { return target; } const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers; observed = new Proxy(target, handlers); toProxy.set(target, observed); toRaw.set(observed, target); return observed; }
稍微精简下
function reactive(target) { const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers observed = new Proxy(target, handlers) return observed}
const collectionTypes = new Set([Set, Map, WeakMap, WeakSet]);
基本上除了 Set, Map, WeakMap, WeakSet,都是 baseHandlers。
baseHandlers实现:
function createGetter(isReadonly, shallow = false) { return function get(target, key, receiver) { const res = Reflect.get(target, key, receiver); if (isSymbol(key) && builtInSymbols.has(key)) { return res; } if (shallow) { track(target, "get" /* GET */, key); // TODO strict mode that returns a shallow-readonly version of the value return res; } if (isRef(res)) { return res.value; } track(target, "get" /* GET */, key); return isObject(res) ? isReadonly // need to lazy access readonly and reactive here to avoid // circular dependency readonly(res) : reactive(res) : res; }; }
返回值如果是 object,就再走一次 reactive,实现深度
下面我们自己写个案例,通过proxy 自定义获取、增加、删除等行为
function reactive(target){ // 创建响应式对象 return createReactiveObject(target);}function isObject(target){ return typeof target === 'object' && target!== null;}function createReactiveObject(target){ // 判断target是不是对象,不是对象不必继续 if(!isObject(target)){ return target; } const handlers = { get(target,key,receiver){ // 取值 console.log('获取') let res = Reflect.get(target,key,receiver); return res; }, set(target,key,value,receiver){ // 更改 、 新增属性 console.log('设置') let result = Reflect.set(target,key,value,receiver); return result; }, deleteProperty(target,key){ // 删除属性 console.log('删除') const result = Reflect.deleteProperty(target,key); return result; } } // 开始代理 observed = new Proxy(target,handlers); return observed;}let p = reactive({name:'zhuanzhuan'});console.log(p.name); // 获取p.name = '转转';// 设置delete p.name;// 删除
我们继续考虑多层对象如何实现代理
let p = reactive({ name: "zhuanzhuan", age: { num: 3 }});p.age.num = 4
由于我们只代理了第一层对象,所以对 age对象进行更改是不会触发 set方法的,但是却触发了 get方法,这是由于 p.age会造成 get操作
get(target, key, receiver) { // 取值 console.log("获取"); let res = Reflect.get(target, key, receiver); return isObject(res) // 懒代理,只有当取值时再次做代理,vue2.0中一上来就会全部递归增加getter,setter ? reactive(res) : res;}
这里我们将 p.age取到的对象再次进行代理,这样在去更改值即可触发 set方法
我们继续考虑数组问题
我们可以发现Proxy默认可以支持数组,包括数组的长度变化以及索引值的变化
let p = reactive([1,2,3,4]);p.push(5);
但是这样会触发两次 set方法,第一次更新的是数组中的第 4项,第二次更新的是数组的 length
看下源码是如何处理的:
很简单,用的 hasOwProperty, set肯定会出发多次,但是通知只出去一次, 比如数组修改 length的时候, hasOwProperty是 true, 那就不触发
function set(target, key, value, receiver) { value = toRaw(value); const oldValue = target[key]; if (isRef(oldValue) && !isRef(value)) { oldValue.value = value; return true; } const hadKey = hasOwn(target, key); const result = Reflect.set(target, key, value, receiver); // don't trigger if target is something up in the prototype chain of original if (target === toRaw(receiver)) { /* istanbul ignore else */ { const extraInfo = { oldValue, newValue: value }; if (!hadKey) { trigger(target, "add" /* ADD */, key, extraInfo); } else if (hasChanged(value, oldValue)) { trigger(target, "set" /* SET */, key, extraInfo); } } } return result; }
我们来屏蔽掉多次触发,更新操作
function hasOwn(target,key){ return target.hasOwnProperty(key);}set(target, key, value, receiver) { // 更改、新增属性 let oldValue = target[key]; // 获取上次的值 let hadKey = hasOwn(target,key); // 看这个属性是否存在 let result = Reflect.set(target, key, value, receiver); if(!hadKey){ // 新增属性 console.log('更新 添加') }else if(oldValue !== value){ // 修改存在的属性 console.log('更新 修改') } // 当调用push 方法第一次修改时数组长度已经发生变化 // 如果这次的值和上次的值一样则不触发更新 return result;}
解决重复使用reactive情况
// 情况1.多次代理同一个对象let arr = [1,2,3,4];let p = reactive(arr);reactive(arr);// 情况2.将代理后的结果继续代理let p = reactive([1,2,3,4]);reactive(p);
通过hash表的方式来解决重复代理的情况
const toProxy = new WeakMap();// 存放被代理过的对象const toRaw = new WeakMap();// 存放已经代理过的对象function reactive(target) { // 创建响应式对象 return createReactiveObject(target);}function isObject(target) { return typeof target === "object" && target !== null;}function hasOwn(target,key){ return target.hasOwnProperty(key);}function createReactiveObject(target) { if (!isObject(target)) { return target; } let observed = toProxy.get(target); if(observed){ // 判断是否被代理过 return observed; } if(toRaw.has(target)){ // 判断是否要重复代理 return target; } const handlers = { get(target, key, receiver) { // 取值 console.log("获取"); let res = Reflect.get(target, key, receiver); return isObject(res) ? reactive(res) : res; }, set(target, key, value, receiver) { let oldValue = target[key]; let hadKey = hasOwn(target,key); let result = Reflect.set(target, key, value, receiver); if(!hadKey){ console.log('更新 添加') }else if(oldValue !== value){ console.log('更新 修改') } return result; }, deleteProperty(target, key) { console.log("删除"); const result = Reflect.deleteProperty(target, key); return result; } }; // 开始代理 observed = new Proxy(target, handlers); toProxy.set(target,observed); toRaw.set(observed,target); // 做映射表 return observed;}
到这里 reactive方法基本实现完毕,接下来就是与 Vue2中的逻辑一样实现依赖收集和触发更新

get(target, key, receiver) { let res = Reflect.get(target, key, receiver); track(target,'get',key); // 依赖收集== return isObject(res) ?reactive(res):res;},set(target, key, value, receiver) { let oldValue = target[key]; let hadKey = hasOwn(target,key); let result = Reflect.set(target, key, value, receiver); if(!hadKey){ trigger(target,'add',key); // 触发添加 }else if(oldValue !== value){ trigger(target,'set',key); // 触发修改 } return result;}
track的作用是依赖收集,收集的主要是 effect,我们先来实现 effect原理,之后再完善 track和 trigger方法
effect实现
effect意思是副作用,此方法默认会先执行一次。如果数据变化后会再次触发此回调函数。
const p = reactive({name:'zhuanzhuan'})effect(()=>{ console.log(p.name); // zhuanzhuan})
我们来实现 effect方法,我们需要将 effect方法包装成响应式 effect。
const activeReactiveEffectStack = []; // 存放响应式effectfunction effect(fn) { const effect = createReactiveEffect(fn); // 创建响应式的 effect effect(); // 先执行一次 return effect;}function createReactiveEffect(fn) { const effect = function() { // 响应式的effect return run(effect, fn); }; return effect;}function run(effect, fn) { try { activeReactiveEffectStack.push(effect); return fn(); // 先让fn执行,执行时会触发get方法,可以将effect存入对应的key属性 } finally { activeReactiveEffectStack.pop(effect); }}
当调用 fn()时可能会触发 get方法,此时会触发 track
const targetMap = new WeakMap();function track(target,type,key){ // 查看是否有effect const effect = activeReactiveEffectStack[activeReactiveEffectStack.length-1]; if(effect){ let depsMap = targetMap.get(target); if(!depsMap){ // 不存在map targetMap.set(target,depsMap = new Map()); } let dep = depsMap.get(target); if(!dep){ // 不存在set depsMap.set(key,(dep = new Set())); } if(!dep.has(effect)){ dep.add(effect); // 将effect添加到依赖中 } }}
当更新属性时会触发 trigger执行,找到对应的存储集合拿出 effect依次执行
我们发现如下问题
function trigger(target,type,key){ const depsMap = targetMap.get(target); if(!depsMap){ return } let effects = depsMap.get(key); if(effects){ effects.forEach(effect=>effect()) }}
新增了值, effect方法并未重新执行,因为 push中修改 length已经被我们屏蔽掉了触发 trigger方法,所以当新增项时应该手动触发 length属性所对应的依赖。
function trigger(target, type, key) { const depsMap = targetMap.get(target); if (!depsMap) { return; } let effects = depsMap.get(key); if (effects) { effects.forEach(effect => effect()); } // 处理如果当前类型是增加属性,如果用到数组的length的effect应该也会被执行 if (type === "add") { let effects = depsMap.get("length"); if (effects) { effects.forEach(effect => { effect(); }); } }
ref实现
ref可以将原始数据类型也转换成响应式数据,需要通过 .value属性进行获取值
function convert(val) { return isObject(val) ? reactive(val) : val;}function ref(raw) { raw = convert(raw); const v = { _isRef:true, // 标识是ref类型 get value() { track(v, "get", ""); return raw; }, set value(newVal) { raw = newVal; trigger(v,'set',''); } }; return v;}
问题又来了我们再编写个案例
let r = ref(1);let c = reactive({ a:r});console.log(c.a.value);
这样做的话岂不是每次都要多来一个.value,这样太难用了
在 get方法中判断如果获取的是 ref的值,就将此值的 value直接返回即可
let res = Reflect.get(target, key, receiver);if(res._isRef){ return res.value}
computed实现
computed 实现也是基于 effect 来实现的,特点是 computed中的函数不会立即执行,多次取值是有缓存机制的
先来看用法:
let a = reactive({name:'zhuanzhuan'});let c = computed(()=>{ console.log('执行次数') return a.name +'今年3岁了';})// 不取不执行,取n次只执行一次console.log(c.value);console.log(c.value);
function computed(getter){ let dirty = true; const runner = effect(getter,{ // 标识这个effect是懒执行 lazy:true, // 懒执行 scheduler:()=>{ // 当依赖的属性变化了,调用此方法,而不是重新执行effect dirty = true; } }); let value; return { _isRef:true, get value(){ if(dirty){ value = runner(); // 执行runner会继续收集依赖 dirty = false; } return value; } }}
修改 effect方法
function effect(fn,options) { let effect = createReactiveEffect(fn,options); if(!options.lazy){ // 如果是lazy 则不立即执行 effect(); } return effect;}function createReactiveEffect(fn,options) { const effect = function() { return run(effect, fn); }; effect.scheduler = options.scheduler; return effect;}
在 trigger时判断
deps.forEach(effect => { if(effect.scheduler){ // 如果有scheduler 说明不需要执行effect effect.scheduler(); // 将dirty设置为true,下次获取值时重新执行runner方法 }else{ effect(); // 否则就是effect 正常执行即可 }});
let a = reactive({name:'zhuanzhuan'});let c = computed(()=>{ console.log('执行次数') return a.name +'今年3岁了';})// 不取不执行,取n次只执行一次console.log(c.value);a.name = '转转'; // 更改值 不会触发重新计算,但是会将dirty变成trueconsole.log(c.value); // 重新调用计算方法
实现 vue-next 极简的响应式系统
直接拷贝下面代码,去运行看效果吧。推荐使用高版本的chrome浏览器!
my-vue-next.js 文件
// 存放被代理过的对象let toProxy = new WeakMap()// 存放已经代理过的对象let toRaw = new WeakMap()let tagetMap = new WeakMap()let effectStack = []const baseHander = { get(target, key){ const res = Reflect.get(target, key) // 收集依赖 track(target, key) // 递归寻找 return typeof res == 'object' ? reactive(res) : res }, set(target, key, val){ const info = {oldValue: target[key], newValue:val} const res = Reflect.set(target, key, val) // 触发更新 trigger(target, key, info) return res }}function reactive(target){ // 查询缓存 let observed = toProxy.get(target) if(observed){ return observed } // 如果已经代理过了这个对象,则直接返回代理后的结果即可 if(toRaw.get(target)){ return target } observed = new Proxy(target, baseHander) // 设置缓存 toProxy.set(target, observed) toRaw.set(observed, target) return observed}function trigger(target, key, info){ // 触发更新 const depsMap = tagetMap.get(target) if(depsMap===undefined){ return } const effects = new Set() const computedRunners = new Set() if(key){ let deps = depsMap.get(key) if(!deps) return deps.forEach(effect=>{ if(effect.computed){ computedRunners.add(effect) }else{ effects.add(effect) } }) } effects.forEach(effect=> effect()) computedRunners.forEach(effect=> effect())}function track(target, key){ let effect = effectStack[effectStack.length - 1] if(effect){ let depsMap = tagetMap.get(target) if(depsMap===undefined){ depsMap = new Map() tagetMap.set(target, depsMap) } let dep = depsMap.get(key) if(dep===undefined){ dep = new Set() depsMap.set(key, dep) } if(!dep.has(effect)){ dep.add(effect) } }}// 存储effectfunction effect(fn,options={}){ let e = createReactiveEffect(fn, options) // 首次页面加载就需要先运行一次 effect 方法,让页面渲染 if(!options.lazy){ e() } return e}function createReactiveEffect(fn,options){ const effect = function(...args){ return run(effect, fn , args) } // 为了调试查看 effect.fn = fn effect.computed = options.computed effect.lazy = options.lazy return effect}function run(effect, fn , args){ if(effectStack.indexOf(effect)===-1){ try{ effectStack.push(effect) return fn(...args) } finally{ effectStack.pop() } }}function computed(fn){ const runner = effect(fn,{computed:true, lazy:true}) return { effect:runner, get value(){ return runner() } }}
index.html 文件
Title点我
推荐Vue学习资料文章:
《「干货」Vue+高德地图实现页面点击绘制多边形及多边形切割拆分》
《「干货」Vue+Element前端导入导出Excel》
《「实践」Deno bytes 模块全解析》
《细品pdf.js实践解决含水印、电子签章问题「Vue篇」》
《基于vue + element的后台管理系统解决方案》
《Vue仿蘑菇街商城项目(vue+koa+mongodb)》
《基于 electron-vue 开发的音乐播放器「实践」》
《「实践」Vue项目中标配编辑器插件Vue-Quill-Editor》
《基于 Vue 技术栈的微前端方案实践》
《消息队列助你成为高薪 Node.js 工程师》
《Node.js 中的 stream 模块详解》
《「干货」Deno TCP Echo Server 是怎么运行的?》
《「干货」了不起的 Deno 实战教程》
《「干货」通俗易懂的Deno 入门教程》
《Deno 正式发布,彻底弄明白和 node 的区别》
《「实践」基于Apify+node+react/vue搭建一个有点意思的爬虫平台》
《「实践」深入对比 Vue 3.0 Composition API 和 React Hooks》
《前端网红框架的插件机制全梳理(axios、koa、redux、vuex)》
《深入Vue 必学高阶组件 HOC「进阶篇」》
《深入学习Vue的data、computed、watch来实现最精简响应式系统》
《10个实例小练习,快速入门熟练 Vue3 核心新特性(一)》
《10个实例小练习,快速入门熟练 Vue3 核心新特性(二)》
《教你部署搭建一个Vue-cli4+Webpack移动端框架「实践」》
《2020前端就业Vue框架篇「实践」》
《详解Vue3中 router 带来了哪些变化?》
《Vue项目部署及性能优化指导篇「实践」》
《Vue高性能渲染大数据Tree组件「实践」》
《尤大大细品VuePress搭建技术网站与个人博客「实践」》
《10个Vue开发技巧「实践」》
《是什么导致尤大大选择放弃Webpack?【vite 原理解析】》
《带你了解 vue-next(Vue 3.0)之 小试牛刀【实践】》
《带你了解 vue-next(Vue 3.0)之 初入茅庐【实践】》
《实践Vue 3.0做JSX(TSX)风格的组件开发》
《一篇文章教你并列比较React.js和Vue.js的语法【实践】》
《手拉手带你开启Vue3世界的鬼斧神工【实践】》
《深入浅出通过vue-cli3构建一个SSR应用程序【实践】》
《怎样为你的 Vue.js 单页应用提速》
《聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总》
《【新消息】Vue 3.0 Beta 版本发布,你还学的动么?》
《Vue真是太好了 壹万多字的Vue知识点 超详细!》
《Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5》
《深入浅出Vue3 跟着尤雨溪学 TypeScript 之 Ref 【实践】》
《手把手教你深入浅出vue-cli3升级vue-cli4的方法》
《Vue 3.0 Beta 和React 开发者分别杠上了》
《手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件》
《Vue3 尝鲜》
《总结Vue组件的通信》
《手把手让你成为更好的Vue.js开发人员的12个技巧和窍门【实践】》
《Vue 开源项目 TOP45》
《2020 年,Vue 受欢迎程度是否会超过 React?》
《尤雨溪:Vue 3.0的设计原则》
《使用vue实现HTML页面生成图片》
《实现全栈收银系统(Node+Vue)(上)》
《实现全栈收银系统(Node+Vue)(下)》
《vue引入原生高德地图》
《Vue合理配置WebSocket并实现群聊》
《多年vue项目实战经验汇总》
《vue之将echart封装为组件》
《基于 Vue 的两层吸顶踩坑总结》
《Vue插件总结【前端开发必备】》
《Vue 开发必须知道的 36 个技巧【近1W字】》
《构建大型 Vue.js 项目的10条建议》
《深入理解vue中的slot与slot-scope》
《手把手教你Vue解析pdf(base64)转图片【实践】》
《使用vue+node搭建前端异常监控系统》
《推荐 8 个漂亮的 vue.js 进度条组件》
《基于Vue实现拖拽升级(九宫格拖拽)》
《手摸手,带你用vue撸后台 系列二(登录权限篇)》
《手摸手,带你用vue撸后台 系列三(实战篇)》
《前端框架用vue还是react?清晰对比两者差异》
《Vue组件间通信几种方式,你用哪种?【实践】》
《浅析 React / Vue 跨端渲染原理与实现》
《10个Vue开发技巧助力成为更好的工程师》
《手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】》
《1W字长文+多图,带你了解vue的双向数据绑定源码实现》
《深入浅出Vue3 的响应式和以前的区别到底在哪里?【实践】》
《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》
《基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截原理与实现》
《手把手教你D3.js 实现数据可视化极速上手到Vue应用》
《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【上】》
《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【中】》
《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【下】》
《Vue3.0权限管理实现流程【实践】》
《后台管理系统,前端Vue根据角色动态设置菜单栏和路由》
作者:大转转FE
转发链接:https://mp.weixin.qq.com/s/Jb5NKXNhlb1Enq0R1p9DcA