在2019.10.5日发布了Vue3.0
预览版源码,但是预计最早需要等到 2020 年第一季度才有可能发布 3.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
1.剖析Vue Composition API
可以去看官方地址
- Vue 3 使用
ts
实现了类型推断,新版api全部采用普通函数,在编写代码时可以享受完整的类型推断(避免使用装饰器) - 解决了多组件间逻辑重用问题 (解决:高阶组件、mixin、作用域插槽)
- Composition API 使用简单
先尝鲜Vue3.0看看效果
<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:`` } Vue.createApp().mount(App,container); script>
到这里你会发现响应式才是
Vue
的灵魂
2.源码目录剖析
packages目录中包含着Vue3.0
所有功能
├── packages
│ ├── compiler-core # 所有平台的编译器
│ ├── compiler-dom # 针对浏览器而写的编译器
│ ├── reactivity # 数据响应式系统
│ ├── runtime-core # 虚拟 DOM 渲染器 ,Vue 组件和 Vue 的各种API
│ ├── runtime-dom # 针对浏览器的 runtime。其功能包括处理原生 DOM API、DOM 事件和 DOM 属性等。 │ ├── runtime-test # 专门为测试写的runtime │ ├── server-renderer # 用于SSR │ ├── shared # 帮助方法 │ ├── template-explorer │ └── vue # 构建vue runtime + compiler
compilercompiler-core
主要功能是暴露编译相关的API
以及baseCompile
方法compiler-dom
基于compiler-core
封装针对浏览器的compiler
(对浏览器标签进行处理)
runtimeruntime-core
虚拟 DOM 渲染器、Vue 组件和 Vue 的各种APIruntime-test
将DOM
结构格式化成对象,方便测试runtime-dom
基于runtime-core
编写的浏览器的runtime
(增加了节点的增删改查,样式处理等),返回render
、createApp
方法
reactivity
单独的数据响应式系统,核心方法reactive
、effect
、 ref
、computed
vue
整合 compiler
+ runtime
到此我们解析了
Vue3.0
结构目录,整体来看整个项目还是非常清晰的
再来尝尝鲜:
我们可以根据官方的测试用例来看下如何使用Vue3.0
const app = {
template:`{{count}}`,
data(){
return {count:100} }, } let proxy = Vue.createApp().mount(app,container); setTimeout(()=>{ proxy.count = 200; },2000)
接下来我们来对比 Vue 2 和 Vue 3 中的响应式原理区别
3.Vue2.0响应式原理机制 - defineProperty
这个原理老生常谈了,就是拦截对象
,给对象的属性增加set
和 get
方法,因为核心是defineProperty
所以还需要对数组的方法进行拦截
3.1 对对象进行拦截
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';
3.2 数组方法劫持
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)
这里依赖收集的过程就不详细描述了,我们把焦点放在
Vue3.0
上
-
Object.defineProperty缺点
- 无法监听数组的变化
- 需要深度遍历,浪费内存
4.Vue3.0数据响应机制 - Proxy
在学习Vue3.0之前,你必须要先熟练掌握ES6中的 Proxy、Reflect 及 ES6中为我们提供的 Map、Set两种数据结构
先应用再说原理:
let p = Vue.reactive({name:'youxuan'});
Vue.effect(()=>{ // effect方法会立即被触发 console.log(p.name); }) p.name = 'webyouxuan';; // 修改属性后会再次触发effect方法
源码是采用
ts
编写,为了便于大家理解原理,这里我们采用js来从0编写,之后再看源码就非常的轻松啦!
4.1 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
方法
4.2 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); // 创建响应式的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){ // 查看是否有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(); }) } }
我们发现如下问题
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(); }); } } }
4.3 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
}
4.4 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:()=>{ // 当依赖的属性变化了,调用此方法,而不是重新执行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变成true console.log(c.value); // 重新调用计算方法
到此我们将Vue3.0
核心的Composition Api
就讲解完毕了! 不管是面试还是后期的应用也再也不需要担心啦!~