在2019.10.5日发布了Vue3.0预览版源码,可以直接看 github源码。
新版Vue 3.0计划并已实现的主要架构改进和新功能:
- 编译器(Compiler)
- 使用模块化架构
- 优化 “Block tree”
- 更激进的 static tree hoisting 功能 (检测静态语法,进行提升)
- 支持 Source map
- 内置标识符前缀(又名”stripWith”)
- 内置整齐打印(pretty-printing)功能
- 移除 Source map 和标识符前缀功能后,使用 Brotli 压缩的浏览器版本精简了大约10KB
运行时(Runtime)
- 速度显著提升
- 同时支持 Composition API 和 Options API,以及 typings
- 基于 Proxy 实现的数据变更检测
- 支持 Fragments (允许组件有从多个根结点)
- 支持 Portals (允许在DOM的其它位置进行渲染)
支持 Suspense w/ async setup()
目前不支持
IE11说明:
Vue2.0响应式原理机制 -defineProperty,这个原理老生常谈了,就是拦截对象,给对象的属性增加set和get方法,因为核心是defineProperty所以还需要对数组的方法进行拦截,Object.defineProperty缺点- 无法监听数组的变化
- 需要深度遍历,浪费内存
在学习Vue3.0之前,你必须要先熟练掌握ES6中的 Proxy、Reflect 及 ES6中为我们提供的 Map、Set两种数据结构.
Vue3.0响应式原理机制- Proxy
先应用再说原理:
let p = Vue.reactive({name:'youxuan'});Vue.effect(()=>{ // effect方法会立即被触发console.log(p.name);})p.name = 'webyouxuan';; // 修改属性后会再次触发effect方法
源码是采用ts编写,为了便于大家理解原理,这里我们采用js来从0编写,之后再看源码就非常的轻松啦!
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; // 删除
我们继续考虑多层对象如何实现代理
由于我们只代理了第一层对象,所以对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实现
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); // 创建响应式的effecteffect(); // 先执行一次return effect;}const activeReactiveEffectStack = []; // 存放响应式effectfunction createReactiveEffect(fn) {const effect = function() {// 响应式的effectreturn 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){ // 不存在maptargetMap.set(target,depsMap = new Map());}let dep = depsMap.get(target);if(!dep){ // 不存在setdepsMap.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实现
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
问题又来了我们再编写个案例
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:'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:()=>{ // 当依赖的属性变化了,调用此方法,而不是重新执行effectdirty = 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 说明不需要执行effecteffect.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); // 重新调用计算方法
到此我们将Vue3.0核心的 Composition Api 就完毕了
