day-098-ninety-eight-20230624-vue-响应式处理思路-仿源码
vue
vue大体概念
-
Vue是渐进式框架
- 所谓渐进式框架,就是把一套全面的框架设计体系,拆分成为多个框架,项目中需要用到那些需求,再导入对应的框架,以此来保证外部资源的最小化!
-
Vue2全家桶
- Vue@2:vue框架的核心!含单个组件状态管理、组件的管理。
- vue-cli:用于创建项目的脚手架工具。管控webpack等打包功能。
- vuex@3:实现vue组件间的公共状态管理。
- vuex-persist 公共状态持久化存储插件。
- …
- vue-router@3:SPA单页面应用中的路由管理!
- UI组件库:
- PC端:饿了么团队element-ui、阿里antd of vue@1、京东iview。
- 移动端:有赞vant@2、蚂蚁金服cube…
- …
- Vue@2:vue框架的核心!含单个组件状态管理、组件的管理。
-
Vue3全家桶
- vue@3
- vite:用于创建项目的脚手架工具。
- vuex@4、pinia
- vue-router@4
- UI组件库:
- PC端:element-plus、antd of vue@3…
- 移动端:vant@3…
- …
- vue@3
-
Vue生态中,完善的项目解决方案:
- antd pro vue:淘系方案-核心是vue3。
- pro.antdv官网
- 免费版:pro.antdv文档
- 收费版:vue3 + TS
- 若依
- 若依-官网
- …
- antd pro vue:淘系方案-核心是vue3。
vue常见面试题
- Vue2框架常见的面试题
- 谈谈你对 MVVM / MVC 模式的理解
- Vue2框架怎么实现对象和数组的监听?「Vue2响应式原理」
- v-model指令实现的原理
- v-show 与 v-if 有什么区别?
- Class 与 Style 如何动态绑定?
- computed 和 watch 的区别和运用的场景?
- 谈谈你对 Vue2 生命周期的理解?
- Vue怎么用 vm.$set() 解决对象新增属性不能响应的问题 ?
- 开发中常用的Vue指令有哪些
MVVM与MVC
- 面试题:谈谈你对 MVVM / MVC 模式的理解?
-
MVVM模式:双向数据驱动,如Vue2与Vue3。
-
model:数据层。
- 在数据层,我们需要构建出:项目中需要的各种数据与方法。例如:响应式状态、属性、计算属性、监听器、过滤器、方法、钩子函数…
- 说明:
-
在vue2中:基于OptionsAPI(配置项)方式,来管理这些内容。
export default {data(){ return { ... } },props:[...],computed:{},watch:{},filters:{},methods:{},... }
<script> export default {data(){ return { ...响应式状态 } },props:[...属性],computed:{...计算属性},watch:{...监听器},filters:{...过滤器},methods:{...方法},... } </script>
-
vue3中:基于CompositionAPI(聚合式)&函数式编程方式,来管理这些内容。
-
- 说明:
- 在数据层,我们需要构建出:项目中需要的各种数据与方法。例如:响应式状态、属性、计算属性、监听器、过滤器、方法、钩子函数…
-
view 视图层。
- 视图层的原理:在
Vue框架
中,我们基于<template>
或jsx语法
构建需要的视图,最后把视图编译为VirtualDOM
(虚拟DOM
),再经过DOM-diff
进行差异化对比,最后把VirtualDOM
/补丁包
渲染为真实的DOM
。- 步骤说明:
- 基于
<template>
或jsx语法
构建需要的视图。- 这个主要是用户自己手写的,绑定响应式数据与绑定事件。
- 还基于指令控制数据与视图的联系。
- 把
视图
编译为VirtualDOM
。- 在vue2中:基于
vue-template-compiler插件
,把视图
编译为VirtualDOM
。 - 在vue3中:基于
@vue/compiler-sfc插件
,把视图
编译为VirtualDOM
。
- 在vue2中:基于
- 经过
DOM-diff
进行差异化对比。- 这个是vue内部做的,diff算法。
- 把
VirtualDOM
/补丁包
渲染为真实的DOM
。- 渲染周期步骤:
第一次渲染
是VirtualDOM
直接渲染为真实的DOM
。非初次渲染
是补丁包
渲染为真实的DOM
。补丁包
是通过DOM-diff
这一步来对比新旧数据
来生成的。性能好,能更快渲染。
- 这一步基本上都是vue内部做的。
- 渲染周期步骤:
- 基于
- 步骤说明:
- 视图层的原理:在
-
viewModel:监听层,Vue框架的核心。
- 这个是vue框架内部自动做的。正常不用关心。
- 监听响应式数据的变化,当数据发生改变后,通知视图更新。
- Vue2中基于Object.defineProperty对数据进行劫持。
- Vue3中基于ES6中的Proxy对数据进行劫持。
- 基于观察者模式通知视图更新。
- 监听视图的变化(一般指的是Form表单内容的改变),当视图内容改变后,自动修改对应的数据(数据一改,视图紧接着跟着更新)。
- 监听视图变化主要是基于v-model指令。
-
-
MVC模式:单向数据驱动框架,如React。
-
model 数据层。
- 构建项目中需要的数据和方法。例如:状态、属性、钩子函数、普通函数等。
- 类组件中:基于
state
/props
/实例
构建状态和属性。 - 函数组件中:基于
useState
/useEffect
等Hooks函数
,完成上述内容的管理。
- 类组件中:基于
- 构建项目中需要的数据和方法。例如:状态、属性、钩子函数、普通函数等。
-
view 视图层。
- 在React中,基于
jsx语法
构建需要的视图
。React
会基于babel-preset-react-app
把jsx语法
编译为React.createElement格式
,createElement方法
执行,会创建出对应的VirtualDOM
,经过DOM-diff对比
,最后把VirtualDOM
/补丁包
,基于ReactDOM中的render方法
,渲染为真实的DOM
!
- 在React中,基于
-
controller 控制层。
- 实现事件绑定和相关的业务逻辑。
- React框架实现了
数据更改可以让视图自动更新
的机制。但是React不同于Vue,并没有对状态做数据劫持。如果打算修改状态后,让视图更新,需要基于特定的方法去修改状态才可以!- 类组件中:可以用setState/forceUpdate方法。
- 函数组件中,可以用useState等Hook函数。
- 但是React中默认并没有实现对视图的监听,这样导致,视图内容改变,对应的状态也不会自动更改!
- 不过我们可以自己给表单元素做事件绑定,当内容改变后,手动去修改对应的状态。
-
-
总结:无论是
MVVM
还是MVC
,都是目前前端主流的框架思想,都是以数据驱动视图渲染
为核心
,告别传统直接操作DOM的方式
,转而操作VirtualDOM
!再配合对应的生态体系,让项目开发既高效,又提高了性能!…
-
Vue的学习路线
- 如何学习Vue?
- 第一条线:视图线
<template>
或JSX语法
- 指令「内置的14个指令和自定义指令」
- JSX语法
- VirtualDOM编译的机制
- 掌握DOM-DIFF算法
- …
- 第二条线:数据线
- 学习
OptionsAPI
/CompositionAPI
中的:语法、原理、区别等内容- OptionsAPI选项
- 学习 MVVM 的原理
- 数据是如何被监听的「Vue2和Vue3是不一样的」
- 监听数据变化后,如何通知视图更新「观察者模式」
- 如何监听视图的变化,以及如何让状态跟着更改「v-model」
- 学习
- 第三条线:组件化开发
- 单文件组件「含样式私有化方案的原理」
- 类组件和函数组件
- 复合组件通信
- 组件封装的技巧「各种封装技巧」
- 通用业务组件
- UI组件库的二次封装
- 通用功能组件
- Vue.mixin
- Vue.directive
- Vue.extend
- …
- 第四条线:实战线
- vuex / vue-router
- …
<keep-alive>
<transition>
<component>
- 上拉刷新、下拉加载
- 超长列表性能优化
- 登录/权限管理模型
- 前后端数据通信管理方案
- …
- vuex / vue-router
- 第一条线:视图线
OptionsAPI选项式数据
- OptionsAPI选项-数据
- OptionsAPI选项-DOM
- OptionsAPI选项-生命周期钩子
- OptionsAPI选项-资源
- OptionsAPI选项-组合
- OptionsAPI选项-其它
对象和数组的监听
- 面试题:Vue2框架怎么实现对象和数组的监听?「Vue2响应式原理」
数据初始化
- vue2源码在
/node_modules/vue/dist/vue.js
中。 - 在
new Vue()
的时候,OptionsAPI中的data是用来构建响应式数据
-即状态
的。- 特点
- 在data中构建的状态,会直接挂载到实例上。
- 在js中,可以基于实例去访问对应的状态 ->
vm.msg
/this.xxx
; - 而挂载到实例上的信息,可以直接在视图中访问 ->
{{msg}}
;
- 在js中,可以基于实例去访问对应的状态 ->
- 在data中构建的状态,会被进行
数据劫持
,即get
/set
。数据劫持的目的是让其变为响应式的,这样以后修改此状态信息,会触发set劫持函数,在此劫持函数中,不仅修改了状态值,而且还会通知视图更新!
- 只有在new的时候,写在data中的状态,才会默认被数据劫持,变为响应式状态。
- 在data中构建的状态,会直接挂载到实例上。
- Vue2响应式源码:
- 在
new Vue()
后,首先执行Vue.prototype._init方法,在此方法中做了很多事情,例如:- 向实例上挂载很多内置的私有属性。
- 带$xxx是我们开发者后续要用到的。
- 带_xxx是给Vue内部用的。
- 基于callHook$1方法,触发beforeCreate()钩子函数执行。
- 初始化上下文中的信息。
- 执行initState方法,初始化属性、状态、计算属性、监听器等信息。
- 触发created钩子函数执行。
- …
- 向实例上挂载很多内置的私有属性。
- 执行initState方法的时候
- 基于initProps$1初始化属性。注册接收属性与属性规则校验。
- 基于initMethods初始化普通函数。
- 基于initComputed$1初始化计算属性。
- 基于initWatch初始化监听器。
- 基于initData初始化状态。
- …
- 执行initData方法的时候,主要目的就是初始化状态-也就是把信息做响应式数据劫持。
-
先判断data是否是一个函数(组件中的data都是函数),如果是函数,先把函数执行(函数中的this是实例,并且传递实例),把执行的返回值,重新赋值给data。
var data = vm.$options.data; data = vm._data = isFunction(data) ? getData(data, vm) : data || {};
-
接下来要确保data是一个纯粹的对象。
if (!isPlainObject(data)) {data = {};warn$2('data functions should return an object:\n' + 'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm); }
-
然后基于Object.keys方法,获取data对象中的可枚举、非Symbol类型的私有属性,然后判断这些属性,是否出现在methods和props中,如果出现了则报错!原因:methods/pprops中编写的信息,也会直接挂在实例上,如果名字,则相互冲突了!
var keys = Object.keys(data); var props = vm.$options.props; var methods = vm.$options.methods; var i = keys.length; while (i--) {... }
var keys = Object.keys(data); var props = vm.$options.props; var methods = vm.$options.methods; var i = keys.length; while (i--) {var key = keys[i];{if (methods && hasOwn(methods, key)) {warn$2("Method \"".concat(key, "\" has already been defined as a data property."), vm);}}if (props && hasOwn(props, key)) {warn$2("The data property \"".concat(key, "\" is already declared as a prop. ") +"Use prop default value instead.", vm);}else if (!isReserved(key)) {proxy(vm, "_data", key);} }
-
最后基于observe函数,对data对象中的信息进行数据劫持!
var ob = observe(data); ob && ob.vmCount++;
-
学习总结:真实项目中,建议把状态数据,全部事先写在data中(即便不清楚其值,也先写上,可以赋值初始值)。因为只有写在data中的数据,在最开始渲染阶段,才会被做
响应式的数据劫持
。
-
- 执行observe方法的时候,把data对象传递进去。
-
如果data对象已经被处理过,则不会重新处理。
if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {return value.__ob__; }
-
而且data对象必须符合好多条件,才可以去处理:是数组或者对象、并且没有被冻结/密封/阻止扩展、并且不是ref对象,也不是VirtualDOM(vnode)…
if (shouldObserve &&(ssrMockReactivity || !isServerRendering()) &&(isArray(value) || isPlainObject(value)) &&Object.isExtensible(value) &&!value.__v_skip /* ReactiveFlags.SKIP */ &&!isRef(value) &&!(value instanceof VNode)) {return new Observer(value, shallow, ssrMockReactivity); }
简洁处理:
if (... && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value.__v_skip && !isRef(value) && !(value instanceof VNode)) {return new Observer(value, shallow, ssrMockReactivity); }
-
如果符合了全部条件,则创建Obsever类的实例,把data对象传递进去进行处理。
-
学习总结:如果某个写在data中的对象,我们不期望对其内部做劫持处理,此时我们只需要把这个对象基于Object.freeze()冻结即可!
- 因为劫持处理的过程是需要消耗性能和时间的。
- 例如:从服务器获取的数据,我们并没有修改其内部某一项值),让视图更新的需求,那么这些数据压根就不需要做劫持。
- 因为劫持处理的过程是需要消耗性能和时间的。
-
- 执行
new Observer(data)
,对data对象中的每一项
进行数据劫持
!-
但凡被处理过的对象,都会设置一个__ob__属性,属性值是Observer类的实例。
def(value, '__ob__', this);
-
然后判断data是数组还是对象,两者处理的方式是不一样的。
-
如果是对象:
-
基于Object.keys()获取对象所有可枚举、非Symbol类型的私有属性。
-
然后迭代这些成员,对每一个成员,基于defineReactive()做数据劫持。
var keys = Object.keys(value); for (var i = 0; i < keys.length; i++) {var key = keys[i];defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock); }
-
-
如果是数组:
- 在Vue2中,有一个对象arrayMethods,这个对象的特点:
- 对象中有7个方法:push/pop/shift/unshift/splice/sort/reverse;
- 对象.__proto__指向Array.prototype
- 接下来让data这个数组,拥有arrayMethods上的这七个方法。
- 在非IE浏览器中,就是让
data数组.__proto__=arrayMethods
。 - 在IE浏览器中,迭代arrayMethods中的每一个方法,把这些方法作为data数组的私有方法。
- 在非IE浏览器中,就是让
- 当我们以后调用数组这7个方法的时候,用的都是arrayMethods中的这七个方法。
- 调用重写的这7个方法,其内部:
- 获取传递的实参。
- 基于Array.prototype内置的方法实现对应的功能。
- 如果调用的是push/unshift/splice,需要把新增的内容,基于observeArray进行递归处理,实现深度的监听劫持。
- 最后通知视图更新。
- 调用重写的这7个方法,其内部:
- 执行observeArray对传递的data数组,再次进行递归处理。
- 在Vue2中,有一个对象arrayMethods,这个对象的特点:
-
-
- 在defineReactive函数中。
-
首先又对此对象中的某个成员进行校验,验证是否是冻结/密封的,如果是,则不进行数据劫持。
var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) {return; }
-
然后对对象中此成员的值,进行递归处理,目的是进行尝试的监听劫持!
var childOb = !shallow && observe(val, false, mock);
-
最后基于Object.defineProperty()对此对象中的这个成员做get/set劫持。
-
- 在observeArray方法中:
-
迭代数组中的每一项,对每一项再基于observe进行递归处理,实现深度的监听劫持。
for (var i = 0, l = value.length; i < l; i++) {observe(value[i], false, this.mock); }
-
深度的监听劫持:
let vm = new Vue({data: {msg: "哈哈",obj:{x:10,}}, });
- 不仅对
msg
与obj
做了数据劫持
,还对obj这个对象的x属性
也做了数据劫持
。
- 不仅对
-
-
- 总结:Vue2响应式原理,针对数组和对象,有不同的处理情况:
- 如果是对象:基于Object.defineProperty对
对象中的每个成员
(成员特点是可枚举、非Symbol类型),进行深度的监听劫持。- 当修改成员值的时候,触发set劫持函数。在set函数中,不仅修改了成员值,而且还对新修改的值做监听劫持。最主要的是通知视图更新!
- 如果是数组:并不像对象一样,没有对数组中的每个索引项做监听劫持。所以基于索引修改数组某一项的值,视图是不会更新的。而是重写了数组的7个方法-push/shift/unshift/pop/splice/sort/reverse,基于这7个方法修改数组的内容,不仅仅修改了内容,而且对新修改的内容也会做劫持,也会通知视图的更新!最后对数组中的每一项内容,也基于递归的方式,看看是否需要劫持!
- 如果是对象:基于Object.defineProperty对
- 代码示例:
-
fang/f20230624/day0624/test.html
<!DOCTYPE html> <html><head><meta charset="UTF-8" /><title>Document</title></head><body><div id="app">{{msg}}-{{text}}</div></body> </html> <!-- <script src="./node_modules/vue/dist/vue.min.js"></script> --> <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> <script>let vm = new Vue({data: {msg: "哈哈",},});vm.text = "嘿嘿";console.log(`实例:vm-->`, vm);setTimeout(() => {vm.text = "hhh";console.log(2000, `text改值了,但视图并没有自动更新`);}, 2000);setTimeout(() => {vm.msg = "方";console.log(10000, `msg改值了,但视图会自动更新`);}, 10000);vm.$mount("#app"); </script>
-
- 在
- 特点
响应式处理思路-仿源码
-
fang/f20230624/day0624/test.html
<!DOCTYPE html> <html><head><meta charset="UTF-8" /><title>Document</title></head><body><div id="app">{{msg}}-{{text}}</div></body> </html> <!-- <script src="./node_modules/vue/dist/vue.min.js"></script> --> <!-- <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> --> <script>// let vm = new Vue({// data: {// msg: "哈哈",// obj:{// x:10,// }// },// });// vm.text = "嘿嘿";// console.log(`实例:vm-->`, vm);// setTimeout(() => {// vm.text = "hhh";// console.log(2000, `text改值了,但视图并没有自动更新`);// }, 2000);// setTimeout(() => {// vm.msg = "方";// console.log(10000, `msg改值了,但视图会自动更新`);// }, 10000);// vm.$mount("#app"); </script><script src="./test.js"></script>
-
fang/f20230624/day0624/test.js
// 检测是否为纯粹对象。 const toString = Object.prototype.toString; const isPlainObject = function isPlainObject(obj) {if (toString.call(obj) !== "[object Object]") return false;let proto = Object.getPrototypeOf(obj);if (!proto) return true;let Ctor = "constructor" in obj && obj.constructor;return Ctor === Object; };// 给对象设置不可枚举的属性。 const define = function define(obj, key, value) {Object.defineProperty(obj, key, {value,enumerable: false,writable: true,configurable: true,});return obj; };// 通知视图更新的方法。 const notify = function notify() {console.log(`视图更新`); };// 重写数组7个方法的对象。 const arrayProto = Array.prototype; const arrayMethods = Object.create(arrayProto); let methods = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"]; methods.forEach((method) => {let original = arrayProto[method]; //对应Array.prototype上的内置方法。define(arrayMethods, method, function mutator(...args) {// 基于内置的方法,把功能先实现。this-我们要操作的数组let result = original.call(this, ...args);// 对于新增或修改的信息,需要基于递归,进行深层次的监听劫持。let inserted;switch (method) {case "push":case "unshift":inserted = args;break;case "splice":inserted = args.slice(2);break;default:break;}if (inserted) {observeArray(inserted);}// 通知视图更新。notify();return result;}); }); // ary.push(100, 200, 300);// 数据劫持的处理。 const defineReactive = function defineReactive(obj, key, proxy) {// 对成员的规则再次校验。let property = Object.getOwnPropertyDescriptor(obj, key);if (property && property.configurable === false) {return;}// 对此成员的值进行深度处理。observe(obj[key]);// 对此成员进行数据劫持。Object.defineProperty(obj, key, {get: function reactiveGetter() {return proxy[key];},set: function reactiveSetter(newVal) {// 新老值相同,则不进行任何的处理。if (Object.is(newVal, obj[key])) {return;}// 修改值proxy[key] = newVal;// 对新设置的值也要进行深度处理。observe(newVal);// 通知视图更新notify();},}); };// 对数组中的每一项进行响应式处理。 const observeArray = function observeArray(arr) {// 对传递数组中的每一项,都基于observe进行响应式处理。// debugger;arr.forEach((item) => {observe(item);}); }; // 对数组/对象进行响应式处理。 const observe = function observe(data) {let isArray = Array.isArray(data);let isObject = isPlainObject(data);// 如果是数组/对象,并且不是被冻结/密封/阻止扩展的,我们才处理。if ((isArray || isObject) && Object.isExtensible(data)) {// 防止套娃操作。if (data.hasOwnProperty("__ob__")) {return data;}define(data, "__ob__", true);// 数组:重定向其原型指向 & 对数组每一项进行深度处理。if (isArray) {data.__proto__ = arrayMethods; // Object.setPrototypeOf(data, arrayMethods);observeArray(data);}// 对象:迭代对象中的每一项,对每一项都基于defineProperty进行数据劫持。if (isObject) {let keys = Object.keys(data);let proxy = { ...data };keys.forEach((key) => {defineReactive(data, key, proxy);});}}// console.log(`data-->`, data);return data; };// -----做测试。 let data = {msg: "哈哈",obj: {x: 10,y: {z: [100, 200],},},arr: [1, 2, { n: 1000 }], }; data.data = data; observe(data);console.log(`响应式数据:data-->`, data);setTimeout(() => {console.log(`data.arr[1] = "改"`);data.arr[1] = "改"; //视图不更新; }, 2000); setTimeout(() => {console.log(`data.msg = "改"`);data.msg = "改"; //视图更新; }, 1000); setTimeout(() => {console.log(`data.arr.push(4, 5, 6)`);data.arr.push(4, 5, 6); //视图更新; }, 5000); setTimeout(() => {console.log(`data.obj.x = "改了data.obj.x"`);data.obj.x = "改了data.obj.x"; //视图更新; }, 10000);// console.log(`data.arr-->`, data.arr);
进阶参考
- OptionsAPI选项-数据