本章概要

  • Vue 2.x 响应式系统的实现原理
  • Vue 3.0 响应式系统的实现原理
  • 体验 Vue 3.0 响应式系统
  • Vue 3.0 带来的新变化

1.1 Web 前端技术的发展

略,自行百度。

1.2 MV* 模式

MVC 是 Web 开发中应用非常广泛的一种框架模式,之后又演变出 MVP 模式和 MVVM 模式

1.2.1 MVC

在 MVC 模式中,一个应用被分成三个部分,即模型(Model)、视图(View)和控制器(Controller)。

1.2.2 MVP

MVP(Model-View-Presenter)是由经典的 MVC 模式演变而来,他们的基本思想有相同的地方:模型(Model)提供数据,视图(View)负责显示,表示器(Presenter)负责逻辑处理。
区别:在 MVP 中 View 并不直接使用 Model ,他们之间的通信是通过 Presenter 进行的,所有的交互都发生在 Presenter 内部,而在 MVC 中 View 会直接从 Model 中读取数据而不是通过 Controller。

1.2.3 MVVM

MVVM(Model-View-ViewModel)是一种软件框架模式,也是一种简化用户界面的事件驱动编程方式。

1.3 初识 Vue.js

Vue 是一套基于 MVVM 模式的用于构建用户界面的 JavaScript 框架,他是以数据和组件化的思想构建的。

1.3.1 渐进式框架

Vue 是渐进式的 JavaScript 框架。所谓渐进式,就是把框架进行分层设计,每层都是可选的,不同层可以灵活地替换为其它的方案。

1.3.2 响应式系统

MVVM模式最核心的特性就是数据双向绑定,Vue构建了一套响应式系统,可以实现用声明的方式绑定数据,从而在数据变化时自动渲染视图。

1. Vue 2.x 响应式系统的实现原理

Vue 2.x 是利用 Object.defineProperty() 方法为对象添加 get() 和 set() 方法来侦测对象的变化,当获取对象属性值时会调用 get() 方法,当修改对象属性值时会调用 set() 方法,于是可以在 get() 和 set() 方法中添加代码,实现数据与视图的双向绑定。

例1-1

// 对Object.defineProperty方法进行封装
function defineReactive(obj, key, value) {Object.defineProperty(obj, key, {get() {return value;},set(newValue) {if (newValue !== value) {updateView(); //在set方法中触发更新value = newValue;}}});
}
// 对一个对象中所有属性的变化进行侦测
function observer(target) {// 如果不是对象数据类型直接返回if (typeof target !== 'object') {return target;}// 循环遍历对象的所有属性,并将它们转换为getter和setter形式for (let key in target) {defineReactive(target, key, target[key]);}
}
// 模拟更新视图的方法
function updateView() {console.log("更新视图");
}
let user = {name:'张三'};
// 对user对象的所有属性变化进行侦测
observer(user);
user.name = '李四';

运行上诉代码,输出结果如下:

更新视图

例1-1 的代码只是简单地侦测了一个对象的属性变化,并没有考虑到对象属性本身又是一个对象的情形。假设 user 对象中有一个属性 address ,该属性本身也是一个对象。代码如下

let user = {name: '张三', address: {city: '北京'}};
// 对user对象的所有属性变化进行侦测
observer(user);
user.address.city = '天津';

运行上述代码,将看不到任何输出,说明对 address 对象的 city 属性的修改并没有被侦测到。
因此,需要修改例1-1 的代码,当对象的属性也是对象类型时,继续为该属性对象的所有属性添加 get() 和 set() 方法。实现起来也很简单,就是在 defineReactive() 函数中添加一个 observer() 函数的递归调用。代码如下

// 对Object.defineProperty方法进行封装
function defineReactive(obj, key, value) {// 通过递归调用,解决多层对象嵌套的属性侦测问题observer(value);Object.defineProperty(obj, key, {...}...
}

再次运行代码,可以看到“更新视图”的输出。
考虑一下下面的代码:

let user = {name: '张三', address: {city: '北京'}};
// 对user对象的所有属性变化进行侦测
observer(user);
user.address = {city: '天津'};
user.address.city = '成都';

上述代码有两次属性变化,一次是为 address 属性设置了一个对象字面常量,一次是修改 address 对象的 city 属性,但如果运行代码,就会发现只能看到一次“更新视图”。这是因为在修改 address 属性时是为它赋值了一个新的对象,而这个新对象的属性并没有被侦测,因此后面对这个新对象属性值的更改就没有被侦测到。
针对这种情况,可以在 set() 方法中为新的值添加 observer() 调用。如下:

  Object.defineProperty(obj, key, {get() {return value;},set(newValue) {if (newValue !== value) {// 如果newValue是对象类型,则继续侦测该对象的所有属性变化// observer函数中已经有对参数是否是对象类型的判断代码,此处可以省略observer(newValue);updateView(); //在set方法中触发更新value = newValue;}}});

至此,已经解决了对象侦测的问题,但还需要考虑数字侦测的问题。如下

let user = {name: '张三', address: {city: '北京'}, emails: ['zhang@163.com']};
// 对user对象的所有属性变化进行侦测
observer(user);
user.emails.push('zhang@sina.com');

emails 属性是数组类型,当通过push() 方法改变数组内容时,并不会触发对象的 set() 方法的调用。如果想在调用数组方法修改数字内容时得到通知,就需要替换数组原型对象,代码如下:

const arrayPrototype = Array.prototype;
// 使用数组的原型对象创建一个新对象
const proto = Object.create(arrayPrototype);// 改变数组自身内容的方法只有如下7个,对它们进行拦截
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {Object.defineProperty(proto, method, {get() {updateView();// 返回数组原有的方法return arrayPrototype[method];}});});
// 对一个对象中所有属性的变化进行侦测
function observer(target) {// 如果不是对象数据类型直接返回if (typeof target !== 'object') {return target;}if (Array.isArray(target)) {// 如果target是数组,则将数组的原型对象设置为protoObject.setPrototypeOf(target, proto);// 对数组中的元素进行侦测for (let i = 0; i < target.length; i++) {observer(target[i])}return;}// 循环遍历对象的所有属性,并将它们转换为getter和setter形式for (let key in target) {defineReactive(target, key, target[key]);}
}

2. Vue 3.0 响应式系统的实现原理

Vue 2.x 是利用 Object.defineProperty() 方法侦测对象的属性变化,但该方法有一些固有的缺陷:性能较差、在对象上新增属性是无法被侦测的、改变数组的 length 属性是无法被侦测的。
以下是 Vue 3.0 一个创建代理的简单示例。

例1-2

// 处理器对象
const baseHandler = {// 陷阱函数,读取属性值时触发// 参数target是目标对象// 参数property是要获取的属性名// 参数receiver是Proxy对象或者继承Proxy的对象get(target, property, receiver){console.log("获取值");},// 陷阱函数,写入属性值时触发// 参数value是新的属性值set(target, property, value, receiver){console.log("设置值");},// 陷阱函数,删除属性时触发deleteProperty(target, property){console.log("删除属性");}
}
// 目标对象
const target = {name: '张三'};
// 为目标对象创建代理对象
const proxy = new Proxy(target, baseHandler);
// 读取属性值
proxy.name;
// 设置属性值
proxy.name = '李四';
// 删除属性
delete proxy.name;

之后,针对代理对象的相关操作就会触发处理器对象中的对应陷阱函数,在陷阱函数中就可以为目标对象的属性访问添加自定义的业务逻辑。
运行结果如下:

获取值
设置值
删除属性

下面使用 Proxy 模拟实现 Vue 3.0 的响应式系统。

例1-3

// 判断某个值是否是对象的辅助方法
function isObject(val){return val !== null && typeof val === 'object';
}
// 响应式核心方法
function reactive(target){return createReactiveObject(target);
}
// 创建响应式对象的方法
function createReactiveObject(target){// 如果target不是对象则直接返回if(!isObject(target)){return target;}const baseHandler = {get(target, property, receiver){console.log('获取值');const result = Reflect.get(target, property, receiver);return result;},set(target, property, value, receiver){console.log('设置值');const result = Reflect.set(target, property, value, receiver);return result;},deleteProperty(target, property){return Reflect.deleteProperty(target, property);}}const observed = new Proxy(target, baseHandler);return observed;
}
const proxy = reactive({name: '张三'});
proxy.name = '李四';
console.log(proxy.name);

Reflect 是一个内置对象,它提供了可拦截 JavaScript 操作的方法。每个代理陷阱对应一个命名和参数都相同的 Reflect 方法。
上述代码的运行结果为:

设置值
获取值
李四

同样,为了解决多层对象侦测的问题,需要在get 陷阱函数中对返回值做一个判断,如果返回值是一个对象,则为返回值也创建代理对象,这也是一个递归调用。
修改 get 陷阱函数,对返回值进行判断

...const baseHandler = {get(target, property, receiver){console.log('获取值');const result = Reflect.get(target, property, receiver);return isObject(result) ? reactive(result) : result;},set(target, property, value, receiver){console.log('设置值');const result = Reflect.set(target, property, value, receiver);return result;},deleteProperty(target, property){return Reflect.deleteProperty(target, property);}}const observed = new Proxy(target, baseHandler);return observed;
}
...
const proxy = reactive({name: 'vue.js', address: {city: '北京'}});
proxy.address.city = '天津';

访问proxy.address时,会引起 get 陷阱函数,由于 address 属性本身也是一个对象,因此该属性也创建代码。
运行结果如下:

获取值
设置值

考虑下面两种情况,第一种情况的代码如下:

const target = {name: '张三'};
const proxy1 = reactive(target);
const proxy2 = reactive(target);

上述代码对同一个目标对象进行了多次代理,如果每次返回一个不同的代理对象,是没有意义的,要解决这个问题,可以在为目标对象初次创建代理后,以目标对象为 key ,代理为 value ,保存到一个 Map 中,然后在每次创建代理前,对目标进行判断,如果已经存在代理对象,则直接返回代理对象,而不再新建代理对象。
第二种情况的代码如下:

const target = {name: '张三'};
const proxy1 = reactive(target);
const proxy2 = reactive(proxy1);

上述代码对一个目标对象进行了代理,然后又对该代理对象进行了代理,这也是无意义的,也需要进行区分,解决方法与第一种情况类似,不过是以代理对象为 key ,目标对象为 value ,保存到一个 Map 中,然后在每次创建代理前,判断传入的目标对象是否本身也是代理对象,如果是,则直接返回该目标对象(原目标对象的代理对象)。
继续完善 例1-3 ,定义两个 WeakMap 对象,分别保存目标对象到代理的映射,以及代理对象到目标对象的映射,并添加判断逻辑。如下:

...
const toProxy = new WeakMap(); // 存放目标对象=>代理对象
const toRaw = new WeakMap();   // 存放代理对象=>目标对象
...
// 创建响应式对象的方法
function createReactiveObject(target){// 如果target不是对象则直接返回if(!isObject(target)){return target;}const proxy = toProxy.get(target);// 如果目标对象是代理对象,并且有对应的真实对象,那么直接返回if(proxy){return proxy;}// 如果目标对象是代理对象,并且有对应的真实对象,那么直接返回if(toRaw.has(target)){  return target;  // 这里的target是代理对象}const baseHandler = {...}const observed = new Proxy(target, baseHandler);toProxy.set(target, observed);toRaw.set(observed, target);return observed;
}

接下来解决向数组中添加元素而导致 set 陷阱相应两次的问题,如下

const proxy = reactive([1, 2, 3]);
proxy.push(4);

上述代码会导致 set 陷阱触发两次,因为push() 方法向数组中添加元素的同时还会修改数组的长度,因此有两次 set 陷阱的触发:一次是将数组索引为3的位置设置为4而触发;一次是修改数字的length属性为4而触发。假如在 set 陷阱函数中更新视图,那么就会出现更新两次的情况。
为了避免上述情况,需要在 set 陷阱函数中区分是新增属性还是修改属性,同时对属性值的修改做一个判断,如果要修改的属性的新值与旧值相同,则无需进行任何操作。
继续完善例1-3的代码。

...
// 判断当前对象上是否有指定属性
function hasOwn(target, property){return target.hasOwnProperty(property);
}
...
// 创建响应式对象的方法
function createReactiveObject(target){...const baseHandler = {get(target, property, receiver){...},set(target, property, value, receiver){// 判断目标对象上是否已经存在该属性const hasProperty = hasOwn(target, property); const oldValue = Reflect.get(target, property);const result = Reflect.set(target, property, value, receiver);if(!hasProperty){console.log('新增属性')}else if(oldValue !== value){ console.log('修改属性')}return result;},...}...
}

针对上述使用 push() 方法向数组添加元素的代码,修改后的响应式代码只会输出“新增属性”,原因是添加元素后,数字的长度已经是4,当 push() 方法修改数组长度为 4 时,新值和旧值相同,因此不进行任何操作。完善后的响应式代码也避免了对属性值进行无意义的修改。
接下来,就是Vue3.0 中比较难理解的依赖手机了,所谓依赖,就是指当数据发生变化,要通知谁。
Vue3.0 使用了 effect() 函数来包装依赖,称为副作用。 effect() 函数的模拟实现如下:

...
// 保存effect的数组,以栈的形式存储
const effectStack = [];
function effect(fn){// 创建响应式effectconst effect = createReactiveEffect(fn);// 默认先执行一次effect,本质上调用的是传入的fn函数effect();
}
// 创建响应式effect
function createReactiveEffect(fn){// 响应式effectconst effect = function(){  try{// 将effect保存到全局数组effectStack中,以栈的形式存储effectStack.push(effect);return fn();} finally{// 调用完依赖后,删除effecteffectStack.pop();}}return effect;
}
...
const proxy = reactive({name: '张三'});
effect(()=>{console.log(proxy.name);
});
proxy.name = '李四';

上述代码运行的结果为

获取值
张三
修改属性

从输出结果中可以看到,除了默认执行一次的 effect 外,当name 属性发生变化时,effect 并没有被执行。为了在对象属性发生变化时,让 effect 再次执行,需要将对象的属性与 effect 进行关联,这可以采用 Map 来存储。
考虑到一个属性可能会关联多个依赖,那么存储的映射关系应该是以对象属性为 key ,保存所有的 effect 的 Set 对象为 value ,之所以选择 Set 而不是数组,是因为 Set 中不能保存重复的元素。
另外,属性毕竟是对象的属性,不可能脱离对象而单独存在,要跟踪不同对象属性的依赖,还需要一个 WeakMap ,将对象本身最为 key,保存所有属性与依赖映射关系的 Map 最为 value ,存储到这个 WeakMap 中。
定义好数据结构后,就可以编写一个依赖手机函数 track() ,如下:

// 保存对象与其属性依赖关系的Map,key是对象,value是Map
const targetMap = new WeakMap();
// 跟踪依赖
function track(target, property){ // 获取全局数组effectStack中的依赖const effect = effectStack[effectStack.length - 1];// 如果存在依赖if(effect){  // 取出该对象对应的Maplet depsMap = targetMap.get(target);// 如果不存在,则以目标对象为key,新建的Map为value,保存到targetMap中if(!depsMap){targetMap.set(target, depsMap = new Map());}// 从Map中取出该属性对应的所有effectlet deps = depsMap.get(property);// 如果不存在,则以属性为key,新建的Set为value,保存到depsMap中if(!deps){depsMap.set(property, deps = new Set());}// 判断Set中是否已经存在effect,如果没有,则添加到deps中if(!deps.has(effect)){deps.add(effect);}}
}

接下来是当属性发生变化时,触发属性关联的所有 effect 执行,为此,再编写一个 trigger() 函数,代码如下:

// 执行属性关联的所有effect
// 参数type在本例中并没有使用,只是模拟Vue 3.0中的代码,用于区分修改属性还是新增属性
function trigger(target, type, property){const depsMap = targetMap.get(target);if(depsMap){let deps = depsMap.get(property);// 将当前属性关联的所有effect依次执行if(deps){deps.forEach(effect => {effect();});}}
}

依赖收集的函数和触发依赖的函数都写完了,那么自然需要在某个地方去收集依赖和触发依赖执行,收集依赖放到触发器对象的 get 陷阱中,而 触发依赖是在属性发生变化时执行依赖,自然是放到 set 陷阱中。
修改 createReactiveObject() 函数,添加 track() 和 trigger() 函数的调用。如下:

// 创建响应式对象的方法
function createReactiveObject(target){...const baseHandler = {get(target, property, receiver){const result = Reflect.get(target, property, receiver);// 收集依赖track(target, property); return isObject(result) ? reactive(result) : result;},set(target, property, value, receiver){// 判断目标对象上是否已经存在该属性const hasProperty = hasOwn(target, property); const oldValue = Reflect.get(target, property);const result = Reflect.set(target, property, value, receiver);if(!hasProperty){trigger(target, 'add', property);}else if(oldValue !== value){ trigger(target, 'set', property);}return result;},deleteProperty(target, property){return Reflect.deleteProperty(target, property);}}...
}

至此,模拟实现 Vue 3.0 的响应式系统的代码全部编写完毕。

1.3.3 体验 Vue 3.0 响应式系统

例1-4

<!DOCTYPE html>
<html><head><meta charset="GBK"><title>Hello Vue.js</title></head><body><!--View--><div id="app"><button @click="increment">count值:{{ state.count }}</button></div><!-- 引入Vue.js --><script src="https://unpkg.com/vue@next"></script><script>const App = {setup(){// 为目标对象创建一个响应式对象const state = Vue.reactive({count: 0});function increment(){state.count++;}return {state,increment}   }};// 创建应用程序实例,该实例提供应用程序上下文。// 应用程序实例装载的整个组件树将共享相同的上下文.const app = Vue.createApp(App);// 在id为app的DOM元素上装载应用程序实例的根组件app.mount('#app');</script></body>
</html>

在浏览器中打开此页面,运行效果如下:

点击按钮,可以看到计数值在增长。

1.4 Vue 3.0 带来的新变化

Vue 3.0 并没有沿用 Vue 2.x 版本的代码,而是从头重写了,代码采用 TypeScript 进行编写,新版的 API 全部采用普通函数,在编写代码时可以享受完整的类型推断。
Vue 3.0 具有以下 8 个亮点:

1. 更好的性能

Vue 3.0 重写了虚拟 DOM 的实现,并对模版的编译进行了优化,改进了组件初始化的速度,相比 Vue 2.x ,更新速度和内存占用方面都有显著的性能提升。

2. Tree-shaking 支持

对无用的模块进行“剪枝”,仅打包需要的,减少了产品发布版本的大小。Vue 3.0 支持按需引入,而 Vue 2.x 中即时用不到的功能也会打包进来。

3. 组合API(Composition API)

Vue 2.x 使用 mixin 来复用功能,但 mixin 存在的问题是:如果用多了,则很难知道某个功能是从哪个 mixin 混入的。
此外,mixin 的类型推断也很差。Vue 3.0 中新增的 Composition API 可以完美替代 mixiin,让用户课可以更灵活且无副作用地复用代码,且 Composition API 可以很好的进行类型推断。Composition API 解决了多组件间逻辑重用的问题。

4. 碎片(Fragmen)

Vue 2.x 的组件需要有一个唯一的根节点,而在 Vue 3.0 中,这成了历史,组件模版不再需要单个的根节点了,可以有很多个节点。

5. 传送(Teleport)

有时组件模版的一部分在逻辑上属于该组件,而从技术角度来看,最好将模版的这一部分移动到 DOM 中 Vue 应用程序之外的其它位置,使用 Teleport 内置组件可以很容易地实现这个需求。

6. 悬念(Suspense)

Suspense 内置组件可以在嵌套层级中等待嵌套的异步依赖项,支持 async setup() ,支持异步组件。

7. 更好的 TypeScript 支持

Vue 3.0 的代码完全采用 TypeScript 编写,具有更好的类型支持。前端开发人员现在可以采用 TypeScript 开发 Vue 应用,而无需担心兼容性问题,结合支持 Vue 3.0 的 TypeScript 插件,开发更加高效,并可以拥有类型检查、自动补全等功能。

8. 自定义渲染器 API

使用自定义渲染器 API ,用户可以尝试与第三方库集成,如编写 WebGL自定义渲染器。
需要注意的是,Vue 3.0 目前并不支持 IE 11。

一、Vue.js 概述相关推荐

  1. Vue.js 概述与 MVVM 模式

    一.Vue.js 1. Vue.js 是什么 Vue.js 是一个轻巧.高性能.可组件化的 MVVM 库,拥有非常容易上手的 API: Vue.js是一个构建数据驱动的 Web 界面的库. 2. Vu ...

  2. 2017 Vue.js 2快速入门指南

    注意,据部分读者反映本文水多,怕湿身者勿进.后续推荐详解 Vue & Vuex 实践 2017 Vue.js 2快速入门指南翻译自Vue.js 2 Quickstart Tutorial 20 ...

  3. vue事件总线_[面试] 聊聊你对 Vue.js 框架的理解

    作者:yacan8 https://github.com/yacan8/blog/issues/26 本文为一次前端技术分享的演讲稿,所以尽力不贴 Vue.js 的源码,因为贴代码在实际分享中,比较枯 ...

  4. 渐进式框架 Vue.js

    渐进式框架 Vue.js 1. Vue.js 概述 2 快速创建.部署.运行和打包一个 Vue.js 项目 2.1. 创建项目 2.2. 安装依赖 2.3. 项目部署和访问 2.4. 打包 Vue 项 ...

  5. 热烈庆祝《Vue.js 实战教程 V2.x(一)基础篇》上线了!

    热烈庆祝<Vue.js 实战教程 V2.x(一)基础篇>上线了! 课程简介 课程地址:https://edu.csdn.net/course/detail/25641 机构名称:大华软件学 ...

  6. 面试官:聊聊对Vue.js框架的理解

    作者:yacan8 https://github.com/yacan8/blog/issues/26 本文为一次前端技术分享的演讲稿,所以尽力不贴 Vue.js 的源码,因为贴代码在实际分享中,比较枯 ...

  7. 计算机英语介绍项目,【精品文档】614关于计算机专业Vue.js应用程序设计开发介绍简介概述的毕业设计论文英文英语外文文献翻译成品资料:了解Vue.js项目和工具(中英文双语对照)...

    1.本文是中英对照毕业设计论文外文文献翻译,下载后直接可用!省去您找文献.pdf整理成word以及翻译的时间,一辈子也就一次的事!文献引用作者出处信息:Freeman, Adam Pro Vue.js ...

  8. Vue.js过滤器概述

    过滤器 一个 Vue.js 的过滤器本质上是一个函数,这个函数会接收一个值,将其处理并返回.过滤器在指令中由一个管道符 (|) 标记,并可以跟随一个或多个参数: <element directi ...

  9. Vue之概述、基本使用、data数据和if条件渲染

    一.Vue概述 Vue.js是前端三大新框架:Angular.js.React.js.Vue.js之一,Vue.js目前的使用和关注程度在三大框架中稍微胜出,并且它的热度还在递增. Vue.js读音 ...

最新文章

  1. 所有企业要注意了,你随时可能掉进GDPR这个坑里!
  2. 在asp.net中使用异步同步rss
  3. webpack打包生成的map文件_从这十几个方面优化你的 Webpack 配置
  4. 2017中国商业智能行业研究报告
  5. CocosCreator休闲游戏发布到字节跳动平台
  6. 嵌入式linux ucgui,Helper2416开发板移植ucgui(嵌入式linux运行ucgui)
  7. 五年产品经理的转正述职报告(附PPT下载)
  8. (转)这是转型AI的励志故事,从非科班到拿下阿里云栖一等奖!
  9. Linux驱动之设备树(设备树下的LED驱动实验)
  10. Linux环境中第一行dpa,nvdimm
  11. 内网穿透的作用 免费内网穿透有哪些 可以用来干什么
  12. Mac上面有哪些宝藏的软件
  13. MySQL 两张表数据合并
  14. sed 删除/增加文件的某一行
  15. 开放大世界 codelikeme
  16. 20060707-Spatial transformations: Translation confusion
  17. 云南师范大学计算机考研资料汇总
  18. echarts 矩形填充占比图treemap
  19. 如何使用万能地图下载器下载矢量建筑边界
  20. 搭建数字孪生车间需要哪些关键技术?

热门文章

  1. Echarts -- graphic 实现自定义图片/文字
  2. golang支付宝支付生成签名
  3. 伴随诊断试剂的三种开发路径
  4. 小程序源码:最新wordpress黑金壁纸微信小程序 二开修复版-多玩法安装简单
  5. FPV救援四足机器人设计(2)
  6. hdu 4915 Parenthese sequence(贪心,模拟)
  7. 前端+node实现一个简单的聊天室功能
  8. it巨头的组织架构图
  9. 产品需求分析与用户体验设计
  10. 补充 sealtalk 群组分析