尤大大在B站进行了直播讲解Vue3.0的一些新特性,因为时间关系 我并无遇上直播 后期观看了一些视频和文章对3.0的特性有了必定的了解,特此写下此Blog进行记录.vue
ts
实现了类型推断,新版api所有采用普通函数,在编写代码时能够享受完整的类型推断(避免使用装饰器)<script src="vue.global.js"></script> <div id="container"></div> <script> function usePosition(){ // 实时获取鼠标位置 let state = Vue.reactive({x:0,y:0}); function update(e) { state.x= e.pageX state.y = e.pageY } Vue.onMounted(() => { window.addEventListener('mousemove', update) }) Vue.onUnmounted(() => { window.removeEventListener('mousemove', update) }) return Vue.toRefs(state); } const App = { setup(){ // Composition API 使用的入口 const state = Vue.reactive({name:'youxuan'}); // 定义响应数据 const {x,y} = usePosition(); // 使用公共逻辑 Vue.onMounted(()=>{ console.log('当组挂载完成') }); Vue.onUpdated(()=>{ console.log('数据发生更新') }); Vue.onUnmounted(()=>{ console.log('组件将要卸载') }) function changeName(){ state.name = 'webyouxuan'; } return { // 返回上下文,能够在模板中使用 state, changeName, x, y } }, template:`<button @click="changeName">{{state.name}} 鼠标x: {{x}} 鼠标: {{y}}</button>` } Vue.createApp().mount(App,container); </script>
简单能够理解为 将各个功能模块聚合react
无需相似Vue2.0 按照特定的格式划分 将模块的各个功能分散于不一样的生命周期钩子函数中,项目各个模块之间耦合更低。web
首先总结回忆一下2.0中响应式实现的原理机制api
function observer(target){ // 若是不是对象数据类型直接返回便可 if(typeof target !== 'object'){ return target } // 从新定义key for(let key in target){ defineReactive(target,key,target[key]) } } function update(){ console.log('update view') } function defineReactive(obj,key,value){ observer(value); // 有可能对象类型是多层,递归劫持 Object.defineProperty(obj,key,{ get(){ // 在get 方法中收集依赖 return value }, set(newVal){ if(newVal !== value){ observer(value); update(); // 在set方法中触发更新 } } }) } let obj = {name:'youxuan'} observer(obj); obj.name = 'webyouxuan';
首先写一个vue方法,在里面定义所须要的数据,用vue.prototype.obersever注册get和set,遍历全部的obj而后取到每个obj里面每个obj[i],而后判断obj[i]的typeof是否是object,若是是那么从新遍历若是不是那么利用obj.defineproperty进行存取数据,get是用了收集依赖,set里面有一个newvalue是=你的value的,而后渲染render(),这样就注册完setget了,而后获取新的值而后从新渲染。js部分完成。在页面引入写好的js而后new一个vue数组
由于defineProperty
是没法监听数组变化的,因此在vue中其实是经过hack了Array原型上的push
和pop
等方法来进行对数组的监听的缓存
let oldProtoMehtods = Array.prototype; let proto = Object.create(oldProtoMehtods); ['push','pop','shift','unshift'].forEach(method=>{ Object.defineProperty(proto,method,{ get(){ update(); oldProtoMehtods[method].call(this,...arguments) } }) }) function observer(target){ if(typeof target !== 'object'){ return target } // 若是不是对象数据类型直接返回便可 if(Array.isArray(target)){ Object.setPrototypeOf(target,proto); // 给数组中的每一项进行observr for(let i = 0 ; i < target.length;i++){ observer(target[i]) } return }; // 从新定义key for(let key in target){ defineReactive(target,key,target[key]) } }
let obj = {hobby:[{name:'youxuan'},'喝']} observer(obj) obj.hobby[0].name = 'webyouxuan'; // 更改数组中的对象也会触发试图更新 console.log(obj)
数组监听实现app
先把array.prototype
取出来,而后在拷贝一份用obj.create
(拷贝是为了防止在修改的时候影响到原来的原型链),而后定义一个储存着数组方法的数组arr,对arr进行forEach
循环,每次循环给拷贝的对象设置一个重写也就是作一个装饰着模式,原型链自己有数组方法,因此拷贝出来的对象也有那些方法。重写先去把刚开始的原型链上的本来的方法好比push
方法用apply(this,arguments)而后再去触发视图更新,而后把这个prototype
关联到get
上的prototype
,将prototype
替换,这样push
方法就即会执行原来的push
方法又会执行触发视图更新函数
首先必须了解ES6中的Proxy,Reflect及Map,Setthis
Reflectprototype
Reflect 是一个内置的对象,它提供拦截 JavaScript 操做的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,所以它是不可构造的。
Proxy
Proxy 对象用于定义基本操做的自定义行为(如属性查找、赋值、枚举、函数调用等)。
const p = new Proxy(target, handler)
***target
***要使用 Proxy
包装的目标对象(能够是任何类型的对象,包括原生数组,函数,甚至另外一个代理)。
***handler
***一个一般以函数做为属性的对象,各属性中的函数分别定义了在执行各类操做时代理 p
的行为。
let p = Vue.reactive({name:'youxuan'}); Vue.effect(()=>{ // effect方法会当即被触发 console.log(p.name); }) p.name = 'webyouxuan';; // 修改属性后会再次触发effect方法
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:'youxuan'}); console.log(p.name); // 获取 p.name = 'webyouxuan'; // 设置 delete p.name; // 删除
那么如何实现多层代理呢?
let p = reactive({ name: "youxuan", age: { num: 10 } }); p.age.num = 11
因为咱们只代理了第一层对象,因此对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
所以咱们从新修改一下更新操做
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意思是反作用,此方法默认会先执行一次。若是数据变化后会再次触发此回调函数。
let school = {name:'youxuan'} let p = reactive(school); effect(()=>{ console.log(p.name); // youxuan })
咱们来实现effect
方法,咱们须要将effect
方法包装成响应式effect
。
function effect(fn) { const effect = createReactiveEffect(fn); // 建立响应式的effect effect(); // 先执行一次 return effect; } const activeReactiveEffectStack = []; // 存放响应式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){ // 查看是否有effectconst 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(); }) } }
咱们发现以下问题
let school = [1,2,3]; let p = reactive(school); effect(()=>{ console.log(p.length); }) p.push(100);
新增了值,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能够将原始数据类型也转换成响应式数据,须要经过.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
实现也是基于 effect
来实现的,特色是computed
中的函数不会当即执行,屡次取值是有缓存机制的
先来看用法:
let a = reactive({name:'youxuan'}); let c = computed(()=>{ console.log('执行次数') return a.name +'webyouxuan'; }) // 不取不执行,取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:'youxuan'}); let c = computed(()=>{ console.log('执行次数') return a.name +'webyouxuan'; }) // 不取不执行,取n次只执行一次console.log(c.value); a.name = 'zf10'; // 更改值 不会触发从新计算,可是会将dirty变成trueconsole.log(c.value); // 从新调用计算方法