在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看看效果

  1. <script src="vue.global.js"></script>
  2. <div id="container"></div>
  3. <script>
  4. function usePosition(){ // 实时获取鼠标位置
  5. let state = Vue.reactive({x:0,y:0});
  6. function update(e) {
  7. state.x= e.pageX
  8. state.y = e.pageY
  9. }
  10. Vue.onMounted(() => {
  11. window.addEventListener('mousemove', update)
  12. })
  13. Vue.onUnmounted(() => {
  14. window.removeEventListener('mousemove', update)
  15. })
  16. return Vue.toRefs(state);
  17. }
  18. const App = {
  19. setup(){ // Composition API 使用的入口
  20. const state = Vue.reactive({name:'youxuan'}); // 定义响应数据
  21. const {x,y} = usePosition(); // 使用公共逻辑
  22. Vue.onMounted(()=>{
  23. console.log('当组挂载完成')
  24. });
  25. Vue.onUpdated(()=>{
  26. console.log('数据发生更新')
  27. });
  28. Vue.onUnmounted(()=>{
  29. console.log('组件将要卸载')
  30. })
  31. function changeName(){
  32. state.name = 'webyouxuan';
  33. }
  34. return { // 返回上下文,可以在模板中使用
  35. state,
  36. changeName,
  37. x,
  38. y
  39. }
  40. },
  41. template:`<button @click="changeName">{{state.name}} 鼠标x: {{x}} 鼠标: {{y}}</button>`
  42. }
  43. Vue.createApp().mount(App,container);
  44. </script>

到这里你会发现响应式才是Vue的灵魂

2.源码目录剖析

packages目录中包含着Vue3.0所有功能

  1. ├── packages
  2.    ├── compiler-core # 所有平台的编译器
  3.    ├── compiler-dom # 针对浏览器而写的编译器
  4.    ├── reactivity # 数据响应式系统
  5.    ├── runtime-core # 虚拟 DOM 渲染器 ,Vue 组件和 Vue 的各种API
  6.    ├── runtime-dom # 针对浏览器的 runtime。其功能包括处理原生 DOM API、DOM 事件和 DOM 属性等。
  7.    ├── runtime-test # 专门为测试写的runtime
  8.    ├── server-renderer # 用于SSR
  9.    ├── shared # 帮助方法
  10.    ├── template-explorer
  11.    └── vue # 构建vue runtime + compiler

compiler
compiler-core主要功能是暴露编译相关的API以及baseCompile方法
compiler-dom基于compiler-core封装针对浏览器的compiler (对浏览器标签进行处理)

runtime
runtime-core 虚拟 DOM 渲染器、Vue 组件和 Vue 的各种API
runtime-testDOM结构格式化成对象,方便测试
runtime-dom 基于runtime-core编写的浏览器的runtime (增加了节点的增删改查,样式处理等),返回rendercreateApp方法

reactivity
单独的数据响应式系统,核心方法reactiveeffect、 refcomputed

vue
整合 compiler + runtime

到此我们解析了Vue3.0结构目录,整体来看整个项目还是非常清晰的

再来尝尝鲜:
我们可以根据官方的测试用例来看下如何使用Vue3.0

  1. const app = {
  2. template:`<div>{{count}}</div>`,
  3. data(){
  4. return {count:100}
  5. },
  6. }
  7. let proxy = Vue.createApp().mount(app,container);
  8. setTimeout(()=>{
  9. proxy.count = 200;
  10. },2000)

接下来我们来对比 Vue 2 和 Vue 3 中的响应式原理区别

3.Vue2.0响应式原理机制 - defineProperty

这个原理老生常谈了,就是拦截对象,给对象的属性增加set 和 get方法,因为核心是defineProperty所以还需要对数组的方法进行拦截

3.1 对对象进行拦截

  1. function observer(target){
  2. // 如果不是对象数据类型直接返回即可
  3. if(typeof target !== 'object'){
  4. return target
  5. }
  6. // 重新定义key
  7. for(let key in target){
  8. defineReactive(target,key,target[key])
  9. }
  10. }
  11. function update(){
  12. console.log('update view')
  13. }
  14. function defineReactive(obj,key,value){
  15. observer(value); // 有可能对象类型是多层,递归劫持
  16. Object.defineProperty(obj,key,{
  17. get(){
  18. // 在get 方法中收集依赖
  19. return value
  20. },
  21. set(newVal){
  22. if(newVal !== value){
  23. observer(value);
  24. update(); // 在set方法中触发更新
  25. }
  26. }
  27. })
  28. }
  29. let obj = {name:'youxuan'}
  30. observer(obj);
  31. obj.name = 'webyouxuan';

3.2 数组方法劫持

  1. let oldProtoMehtods = Array.prototype;
  2. let proto = Object.create(oldProtoMehtods);
  3. ['push','pop','shift','unshift'].forEach(method=>{
  4. Object.defineProperty(proto,method,{
  5. get(){
  6. update();
  7. oldProtoMehtods[method].call(this,...arguments)
  8. }
  9. })
  10. })
  11. function observer(target){
  12. if(typeof target !== 'object'){
  13. return target
  14. }
  15. // 如果不是对象数据类型直接返回即可
  16. if(Array.isArray(target)){
  17. Object.setPrototypeOf(target,proto);
  18. // 给数组中的每一项进行observr
  19. for(let i = 0 ; i < target.length;i++){
  20. observer(target[i])
  21. }
  22. return
  23. };
  24. // 重新定义key
  25. for(let key in target){
  26. defineReactive(target,key,target[key])
  27. }
  28. }

测试

  1. let obj = {hobby:[{name:'youxuan'},'喝']}
  2. observer(obj)
  3. obj.hobby[0].name = 'webyouxuan'; // 更改数组中的对象也会触发试图更新
  4. console.log(obj)

这里依赖收集的过程就不详细描述了,我们把焦点放在Vue3.0

  • Object.defineProperty缺点

    • 无法监听数组的变化
    • 需要深度遍历,浪费内存

4.Vue3.0数据响应机制 - Proxy

在学习Vue3.0之前,你必须要先熟练掌握ES6中的 ProxyReflect 及 ES6中为我们提供的 MapSet两种数据结构

先应用再说原理:

  1. let p = Vue.reactive({name:'youxuan'});
  2. Vue.effect(()=>{ // effect方法会立即被触发
  3. console.log(p.name);
  4. })
  5. p.name = 'webyouxuan';; // 修改属性后会再次触发effect方法

源码是采用ts编写,为了便于大家理解原理,这里我们采用js来从0编写,之后再看源码就非常的轻松啦!

4.1 reactive方法实现

通过proxy 自定义获取、增加、删除等行为

  1. function reactive(target){
  2. // 创建响应式对象
  3. return createReactiveObject(target);
  4. }
  5. function isObject(target){
  6. return typeof target === 'object' && target!== null;
  7. }
  8. function createReactiveObject(target){
  9. // 判断target是不是对象,不是对象不必继续
  10. if(!isObject(target)){
  11. return target;
  12. }
  13. const handlers = {
  14. get(target,key,receiver){ // 取值
  15. console.log('获取')
  16. let res = Reflect.get(target,key,receiver);
  17. return res;
  18. },
  19. set(target,key,value,receiver){ // 更改 、 新增属性
  20. console.log('设置')
  21. let result = Reflect.set(target,key,value,receiver);
  22. return result;
  23. },
  24. deleteProperty(target,key){ // 删除属性
  25. console.log('删除')
  26. const result = Reflect.deleteProperty(target,key);
  27. return result;
  28. }
  29. }
  30. // 开始代理
  31. observed = new Proxy(target,handlers);
  32. return observed;
  33. }
  34. let p = reactive({name:'youxuan'});
  35. console.log(p.name); // 获取
  36. p.name = 'webyouxuan'; // 设置
  37. delete p.name; // 删除

我们继续考虑多层对象如何实现代理

  1. let p = reactive({ name: "youxuan", age: { num: 10 } });
  2. p.age.num = 11

由于我们只代理了第一层对象,所以对age对象进行更改是不会触发set方法的,但是却触发了get方法,这是由于 p.age会造成 get操作

  1. get(target, key, receiver) {
  2. // 取值
  3. console.log("获取");
  4. let res = Reflect.get(target, key, receiver);
  5. return isObject(res) // 懒代理,只有当取值时再次做代理,vue2.0中一上来就会全部递归增加getter,setter
  6. ? reactive(res) : res;
  7. }

这里我们将p.age取到的对象再次进行代理,这样在去更改值即可触发set方法

我们继续考虑数组问题
我们可以发现Proxy默认可以支持数组,包括数组的长度变化以及索引值的变化

  1. let p = reactive([1,2,3,4]);
  2. p.push(5);

但是这样会触发两次set方法,第一次更新的是数组中的第4项,第二次更新的是数组的length

我们来屏蔽掉多次触发,更新操作

  1. set(target, key, value, receiver) {
  2. // 更改、新增属性
  3. let oldValue = target[key]; // 获取上次的值
  4. let hadKey = hasOwn(target,key); // 看这个属性是否存在
  5. let result = Reflect.set(target, key, value, receiver);
  6. if(!hadKey){ // 新增属性
  7. console.log('更新 添加')
  8. }else if(oldValue !== value){ // 修改存在的属性
  9. console.log('更新 修改')
  10. }
  11. // 当调用push 方法第一次修改时数组长度已经发生变化
  12. // 如果这次的值和上次的值一样则不触发更新
  13. return result;
  14. }

解决重复使用reactive情况

  1. // 情况1.多次代理同一个对象
  2. let arr = [1,2,3,4];
  3. let p = reactive(arr);
  4. reactive(arr);
  5. // 情况2.将代理后的结果继续代理
  6. let p = reactive([1,2,3,4]);
  7. reactive(p);

通过hash表的方式来解决重复代理的情况

  1. const toProxy = new WeakMap(); // 存放被代理过的对象
  2. const toRaw = new WeakMap(); // 存放已经代理过的对象
  3. function reactive(target) {
  4. // 创建响应式对象
  5. return createReactiveObject(target);
  6. }
  7. function isObject(target) {
  8. return typeof target === "object" && target !== null;
  9. }
  10. function hasOwn(target,key){
  11. return target.hasOwnProperty(key);
  12. }
  13. function createReactiveObject(target) {
  14. if (!isObject(target)) {
  15. return target;
  16. }
  17. let observed = toProxy.get(target);
  18. if(observed){ // 判断是否被代理过
  19. return observed;
  20. }
  21. if(toRaw.has(target)){ // 判断是否要重复代理
  22. return target;
  23. }
  24. const handlers = {
  25. get(target, key, receiver) {
  26. // 取值
  27. console.log("获取");
  28. let res = Reflect.get(target, key, receiver);
  29. return isObject(res) ? reactive(res) : res;
  30. },
  31. set(target, key, value, receiver) {
  32. let oldValue = target[key];
  33. let hadKey = hasOwn(target,key);
  34. let result = Reflect.set(target, key, value, receiver);
  35. if(!hadKey){
  36. console.log('更新 添加')
  37. }else if(oldValue !== value){
  38. console.log('更新 修改')
  39. }
  40. return result;
  41. },
  42. deleteProperty(target, key) {
  43. console.log("删除");
  44. const result = Reflect.deleteProperty(target, key);
  45. return result;
  46. }
  47. };
  48. // 开始代理
  49. observed = new Proxy(target, handlers);
  50. toProxy.set(target,observed);
  51. toRaw.set(observed,target); // 做映射表
  52. return observed;
  53. }

到这里reactive方法基本实现完毕,接下来就是与Vue2中的逻辑一样实现依赖收集和触发更新

  1. get(target, key, receiver) {
  2. let res = Reflect.get(target, key, receiver);
  3. + track(target,'get',key); // 依赖收集
  4. return isObject(res)
  5. ?reactive(res):res;
  6. },
  7. set(target, key, value, receiver) {
  8. let oldValue = target[key];
  9. let hadKey = hasOwn(target,key);
  10. let result = Reflect.set(target, key, value, receiver);
  11. if(!hadKey){
  12. + trigger(target,'add',key); // 触发添加
  13. }else if(oldValue !== value){
  14. + trigger(target,'set',key); // 触发修改
  15. }
  16. return result;
  17. }

track的作用是依赖收集,收集的主要是effect,我们先来实现effect原理,之后再完善 tracktrigger方法

4.2 effect实现

effect意思是副作用,此方法默认会先执行一次。如果数据变化后会再次触发此回调函数。

  1. let school = {name:'youxuan'}
  2. let p = reactive(school);
  3. effect(()=>{
  4. console.log(p.name); // youxuan
  5. })

我们来实现effect方法,我们需要将effect方法包装成响应式effect

  1. function effect(fn) {
  2. const effect = createReactiveEffect(fn); // 创建响应式的effect
  3. effect(); // 先执行一次
  4. return effect;
  5. }
  6. const activeReactiveEffectStack = []; // 存放响应式effect
  7. function createReactiveEffect(fn) {
  8. const effect = function() {
  9. // 响应式的effect
  10. return run(effect, fn);
  11. };
  12. return effect;
  13. }
  14. function run(effect, fn) {
  15. try {
  16. activeReactiveEffectStack.push(effect);
  17. return fn(); // 先让fn执行,执行时会触发get方法,可以将effect存入对应的key属性
  18. } finally {
  19. activeReactiveEffectStack.pop(effect);
  20. }
  21. }

当调用fn()时可能会触发get方法,此时会触发track

  1. const targetMap = new WeakMap();
  2. function track(target,type,key){
  3. // 查看是否有effect
  4. const effect = activeReactiveEffectStack[activeReactiveEffectStack.length-1];
  5. if(effect){
  6. let depsMap = targetMap.get(target);
  7. if(!depsMap){ // 不存在map
  8. targetMap.set(target,depsMap = new Map());
  9. }
  10. let dep = depsMap.get(target);
  11. if(!dep){ // 不存在set
  12. depsMap.set(key,(dep = new Set()));
  13. }
  14. if(!dep.has(effect)){
  15. dep.add(effect); // 将effect添加到依赖中
  16. }
  17. }
  18. }

当更新属性时会触发trigger执行,找到对应的存储集合拿出effect依次执行

  1. function trigger(target,type,key){
  2. const depsMap = targetMap.get(target);
  3. if(!depsMap){
  4. return
  5. }
  6. let effects = depsMap.get(key);
  7. if(effects){
  8. effects.forEach(effect=>{
  9. effect();
  10. })
  11. }
  12. }

我们发现如下问题

  1. let school = [1,2,3];
  2. let p = reactive(school);
  3. effect(()=>{
  4. console.log(p.length);
  5. })
  6. p.push(100);

新增了值,effect方法并未重新执行,因为push中修改length已经被我们屏蔽掉了触发trigger方法,所以当新增项时应该手动触发length属性所对应的依赖。

  1. function trigger(target, type, key) {
  2. const depsMap = targetMap.get(target);
  3. if (!depsMap) {
  4. return;
  5. }
  6. let effects = depsMap.get(key);
  7. if (effects) {
  8. effects.forEach(effect => {
  9. effect();
  10. });
  11. }
  12. // 处理如果当前类型是增加属性,如果用到数组的length的effect应该也会被执行
  13. if (type === "add") {
  14. let effects = depsMap.get("length");
  15. if (effects) {
  16. effects.forEach(effect => {
  17. effect();
  18. });
  19. }
  20. }
  21. }

4.3 ref实现

ref可以将原始数据类型也转换成响应式数据,需要通过.value属性进行获取值

  1. function convert(val) {
  2. return isObject(val) ? reactive(val) : val;
  3. }
  4. function ref(raw) {
  5. raw = convert(raw);
  6. const v = {
  7. _isRef:true, // 标识是ref类型
  8. get value() {
  9. track(v, "get", "");
  10. return raw;
  11. },
  12. set value(newVal) {
  13. raw = newVal;
  14. trigger(v,'set','');
  15. }
  16. };
  17. return v;
  18. }

问题又来了我们再编写个案例

  1. let r = ref(1);
  2. let c = reactive({
  3. a:r
  4. });
  5. console.log(c.a.value);

这样做的话岂不是每次都要多来一个.value,这样太难用了

get方法中判断如果获取的是ref的值,就将此值的value直接返回即可

  1. let res = Reflect.get(target, key, receiver);
  2. if(res._isRef){
  3. return res.value
  4. }

4.4 computed实现

computed 实现也是基于 effect 来实现的,特点是computed中的函数不会立即执行,多次取值是有缓存机制的

先来看用法:

  1. let a = reactive({name:'youxuan'});
  2. let c = computed(()=>{
  3. console.log('执行次数')
  4. return a.name +'webyouxuan';
  5. })
  6. // 不取不执行,取n次只执行一次
  7. console.log(c.value);
  8. console.log(c.value);
  1. function computed(getter){
  2. let dirty = true;
  3. const runner = effect(getter,{ // 标识这个effect是懒执行
  4. lazy:true, // 懒执行
  5. scheduler:()=>{ // 当依赖的属性变化了,调用此方法,而不是重新执行effect
  6. dirty = true;
  7. }
  8. });
  9. let value;
  10. return {
  11. _isRef:true,
  12. get value(){
  13. if(dirty){
  14. value = runner(); // 执行runner会继续收集依赖
  15. dirty = false;
  16. }
  17. return value;
  18. }
  19. }
  20. }

修改effect方法

  1. function effect(fn,options) {
  2. let effect = createReactiveEffect(fn,options);
  3. if(!options.lazy){ // 如果是lazy 则不立即执行
  4. effect();
  5. }
  6. return effect;
  7. }
  8. function createReactiveEffect(fn,options) {
  9. const effect = function() {
  10. return run(effect, fn);
  11. };
  12. effect.scheduler = options.scheduler;
  13. return effect;
  14. }

trigger时判断

  1. deps.forEach(effect => {
  2. if(effect.scheduler){ // 如果有scheduler 说明不需要执行effect
  3. effect.scheduler(); // 将dirty设置为true,下次获取值时重新执行runner方法
  4. }else{
  5. effect(); // 否则就是effect 正常执行即可
  6. }
  7. });
  1. let a = reactive({name:'youxuan'});
  2. let c = computed(()=>{
  3. console.log('执行次数')
  4. return a.name +'webyouxuan';
  5. })
  6. // 不取不执行,取n次只执行一次
  7. console.log(c.value);
  8. a.name = 'zf10'; // 更改值 不会触发重新计算,但是会将dirty变成true
  9. console.log(c.value); // 重新调用计算方法

到此我们将Vue3.0核心的 Composition Api 就讲解完毕了! 不管是面试还是后期的应用也再也不需要担心啦!~

快速进阶Vue3.0的更多相关文章

  1. 通过10个实例小练习,快速熟练 Vue3.0 核心新特性

    Vue3.0 发 beta 版都有一段时间了,正式版也不远了,所以真的要学习一下 Vue3.0 的语法了. GitHub 博客地址: https://github.com/biaochenxuying ...

  2. vue3.0新特性以及进阶路线

    Vue3.0新特性/改动 新手学习路线  ===> 起步 1. 扎实的 JavaScript / HTML / CSS 基本功.这是前置条件. 2. 通读官方教程 (guide) 的基础篇.不要 ...

  3. 基于 Vue3.0 Composition Api 快速构建实战项目

    Quick Start 项目源码:https://github.com/Wscats/vue-cli 本项目综合运用了 Vue3.0 的新特性,适合新手学习

  4. 预计2019年发布的Vue3.0到底有什么不一样的地方?

    摘要: Vue 3.0预览. 原文:预计今年发布的Vue3.0到底有什么不一样的地方? 作者:小肆 微信公众号:技术放肆聊 Fundebug经授权转载,版权归原作者所有. 还有几个月距离 vue2 的 ...

  5. Vue3.0脚手架搭建

    https://www.jianshu.com/p/fbcad30031c2 vue3.0官网:https://cli.vuejs.org/zh/guide/ 介绍: notice: 这份文档是对应 ...

  6. Vue3实战系列:Vue3.0 + Vant3.0 搭建种子项目

    最近在用 Vue3 写一个开源的商城项目,开源后让大家也可以用现成的 Vue3 大型商城项目源码来练练手,目前处于开发阶段,过程中用到了 Vant3.0,于是就整理了这篇文章来讲一下如何使用 Vue3 ...

  7. vite + ts 快速搭建 vue3 项目 以及介绍相关特性

    博客地址:https://ainyi.com/98 Vue3.0,One Piece 接下来得抽空好好学习了 vite 尤大在 Vue 3.0 beta 直播中推荐了 vite 的工具,强调:针对Vu ...

  8. vue3系列:vue3.0自定义弹框组件V3Popup|vue3.x手机端弹框组件

    基于Vue3.0开发的轻量级手机端弹框组件V3Popup. 之前有分享一个vue2.x移动端弹框组件,今天给大家带来的是Vue3实现自定义弹框组件. V3Popup 基于vue3.x实现的移动端弹出框 ...

  9. vue3系列:vue3.0自定义虚拟滚动条V3Scroll|vue3模拟滚动条组件

    基于Vue3.0构建PC桌面端自定义美化滚动条组件V3Scroll. 前段时间有分享一个Vue3 PC网页端弹窗组件,今天带来最新开发的Vue3.0版虚拟滚动条组件. V3Scroll 使用vue3. ...

随机推荐

  1. 「CF126B」Password

    题目描述 给定一个字符串 \(S\),我们规定一个字符串 \(P\) 是可行解,\(P\) 需要满足: \(P\) 是 \(S\) 的前缀 \(P\) 是 \(S\) 的后缀 \(P\) 出现在 \( ...

  2. .NET List<T>Conat vs AddRange

    最大的不同在于AddRange改变了它所调用的列表,而Concat创建了一个新的List.因此它们有不同的用途. Concat也是一种适用于任何IEnumerable的扩展方法,并返回一个IEnume ...

  3. 关于length、length()、size()

    length:属性,数组的属性. length(): String的方法,方法体里面是  return value.length; size():集合如list.set.map的方法,返回元素个数.

  4. jqueery easyui tree把已选中的节点数据拼成json或者数组(非常重要)

    jqueery easyui tree把已选中的节点数据拼成json或者数组 pqxhk10级分类:其他被浏览14次2018.01.19  https://wenda.so.com/q/1535702 ...

  5. Linux安装nginx的环境要求

    # Linux下切记不能乱删东西!我把pcre强制删除后,什么命令都不能使用了,系统奔溃,血的教训! nginx是C语言开发,建议在linux上运行,本教程使用Centos6.4作为安装环境. 一.  ...

  6. Core data 如何查看ObjectId

    ObjectId是Core Data 为每一个数据对象提供的唯一ID标识,获取ObjectID.并打印的方法如下: 步骤: 1. 获取ManagedObject 2. ManagedObject -& ...

  7. Vue中复制文本 vue-clipboard2

    附上代码,方便复制粘贴 //复制文本 onCopy(meetingId) { let _this = this; getMeetingDetail(meetingId).then(res => ...

  8. 预备JS执行环境,预执行脚本

    page.evaluateOnNewDocument(pageFunction, ...args) pageFunction <function|string> Function to b ...

  9. Jmeter插件解释

    Jmeter插件解释 1.jp@gc - Actiive Threads Over Time:不同时间活动用户数量展示(图表)  2.jp@gc - AutoStop Listener :自动停止监听 ...

  10. vs2010编译C++ 结构体

    //结构体的测试// CTest.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include <iostream> usi ...