vue 的设计模式 —— MVVM
- M —— Model 模型,即数据
- V —— View 视图,即DOM渲染
- VM —— ViewModel 视图模型,用于实现Model和View的通信,即数据改变驱动视图渲染,监听视图事件修改数据
初次渲染
- 将模板编译为 render 函数 ( webpack 中使用的
vue-loader
插件在开发环境启动项目时会完成编译) - 触发响应式,监听 data 属性触发 getter 和 setter 方法 (主要是getter 方法)
- 执行 render 函数,生成 vnode ,执行 patch(elem, vnode) 完成 DOM 渲染
更新过程【需会画和讲解图】
- 修改 data,触发 setter 方法
- 重新执行 render 函数,生成 newVnode
- 执行 patch(vnode,newVnode) 更新发生变化的 DOM 节点
【重点】异步渲染
vue 的更新过程,是一种异步渲染,即并不是每一点 data 的改变都会立马触发视图更新, 而是会汇总 data 的修改,再一次性更新视图,这样可以减少 DOM 的操作次数,提高性能。
vue 原理的三大核心
一、响应式
vue 的响应式机制是在vue 实例初始化时建立的,即 data 函数中定义的变量,在页面初始化后,都具有响应式对于vue 实例初始化之后新增的属性,不具有响应式,解决方案是改用 $set 的方式新增属性。
监听 data 的变化
【核心】 API-Object.defineProperty
此时只能监听到对象的第一层属性,而无法实现更深层次属性变化的监听。
实现深度监听
- 深层对象属性的监听,通过递归遍历深层对象的属性实现
- 深层数组的监听,通过变更数组原型(为所有改变数组的 api 中加入更新视图),再递归遍历深层数组实现。
// 触发更新视图 function updateView() { console.log("视图更新"); } // 重新定义数组原型 const oldArrayProperty = Array.prototype; // 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型 const arrProto = Object.create(oldArrayProperty); ["push", "pop", "shift", "unshift", "splice"].forEach((methodName) => { arrProto[methodName] = function () { updateView(); // 触发视图更新 oldArrayProperty[methodName].call(this, ...arguments); }; }); // 重新定义属性,监听起来 function defineReactive(target, key, value) { // 深度监听 observer(value); // 核心 API Object.defineProperty(target, key, { get() { return value; }, set(newValue) { if (newValue !== value) { // 深度监听 observer(newValue); // 设置新值 // 注意,value 一直在闭包中,此处设置完之后,再 get 时会获取最新的值 value = newValue; // 触发更新视图 updateView(); } }, }); } // 监听对象属性 function observer(target) { // 不是对象或数组,无需深度监听 if (typeof target !== "object" || target === null) { return target; } // 若是数组,则修改为自定义的添加了视图刷新的数组原型 if (Array.isArray(target)) { target.__proto__ = arrProto; } // 重新定义各个属性(for in 也可以遍历数组) for (let key in target) { defineReactive(target, key, target[key]); } } // 准备数据 const data = { name: "张三", age: 20, info: { address: "北京", // 需要深度监听 }, nums: [10, 20, 30], // 需要深度监听 }; // 监听数据 observer(data); // 测试 data.name = "李四"; // 无需深度监听 data.age = 21; // 无需深度监听 data.x = "100"; // 新增属性,监听不到 —— 需用 Vue.set delete data.name; // 删除属性,监听不到 —— 需用 Vue.delete data.info.address = "上海"; // 对象属性的属性,需要深度监听 data.nums.push(4); // 数组需要深度监听
Object.defineProperty 的缺点
- 深度监听,需要递归到底,一次性计算量大
- 无法监听属性的新增和删除,这会导致以下操作无响应式:
- 对象新增属性
- 对象删除属性
- 通过数组下标修改数组元素的值
- 修改数组的长度
为了弥补以上操作无响应式的缺陷,vue 补充了 set 和 delete 方法。
Vue.set() 和 this.$set() 这两个api的实现原理基本一模一样,都是使用了set函数
$set 的响应式原理
this.$set(this.arr, "3", 7)
对于数组,$set 的参数为数组、数组下标、新的值
,通过调用被 vue 改造过的添加了视图更新的 splice 方法实现响应式,相关vue 源码如下:
// 判断操作目标是否是数组,传入的数组下标是否规范 if (Array.isArray(target) && isValidArrayIndex(key)) { // 若传入的数组下标超过数组长度,则将数组长度增长为传入的下标,以防后续调用splice方法时因下标超出数组长度而报错。 target.length = Math.max(target.length, key) // 使用添加了视图更新的 splice 方法实现响应式 target.splice(key, 1, val) return val }
this.$set(this.obj, "新的属性", "新增的属性的值");
- 对于对象,$set 的参数为
对象、新的属性、新增的属性的值
,通过对新增属性添加深度监听实现响应式,相关vue 源码如下:
// 判断如果key本来就是对象中的一个属性,并且key不是Object原型上的属性, 则此属性已添加过响应式,直接修改值即可。 if (key in target && !(key in Object.prototype)) { target[key] = val return val } // 获取 target对象的 __ob__ 属性 const ob = (target: any).__ob__ // 若 target对象是vue实例对象或者是根数据对象,则抛出错误警告。 if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val } // 若 target 对象的 __ob__ 属性不存在,则 target 不是响应式对象,无需添加响应式监听,直接新增属性赋值即可。( vue给响应式对象都加了 __ob__ 属性,如果一个对象有 __ob__ 属性,则说明这个对象是响应式对象) if (!ob) { target[key] = val return val } // 给新属性添加响应式监听 defineReactive(ob.value, key, val) // 触发视图更新 ob.dep.notify() return val
二、模板编译
vue 文件中支持指令、插值、JS 表达式,还能实现判断、循环,大大便捷了开发,但无法在浏览器中渲染,需要先将其转换成 JS 代码才行,这个转换的过程,即模板编译。
编译过程
- 借助插件
vue-template-compiler
将vue 文件编译成 render 函数 - 执行 render 函数,返回 vnode
- 基于 vnode 执行 patch 和 diff ,完成 DOM 渲染
演示代码
const compiler = require('vue-template-compiler') const template = `<p>{{message}}</p>` const res = compiler.compile(template) console.log(res.render)
得到函数
with(this){return _c('p',[_v(_s(message))])}
- _c 对应插件内定义的函数 createElement
- _v 对应插件内定义的函数 createTextVNode
- _s 对应插件内定义的函数 toString
即实现了模板向 JS 的转换。
with 语法
- 改变 {} 内自由变量的查找规则,将其当做 obj 属性来查找
- 如果找不到匹配的 obj 属性,就会报错
- with 要慎用,它打破了作用域规则,易读性变差
编译形式
- 在 webpack 中使用的
vue-loader
插件,在开发环境启动项目时,就完成了模板的编译(提升了渲染效率) - vue 组件可以用 template 写法,也可以直接用 render 函数(react 中全是 render 函数 )
三、虚拟节点 vDom
数据变化驱动视图更新,就需要执行DOM 操作重新渲染视图,但DOM 操作非常耗费性能,怎样提升性能呢?
解决思路:使用虚拟节点 vDom,即用 JS 模拟 DOM 结构,计算出最小的变更,更新 DOM。
因为 JS 的执行速度比DOM 操作快得多!
通过 h 函数生成 vnode
- 初次渲染【增】
patch(container,vnode);
在目标容器中,渲染节点
- 更新视图【改】
patch(vnode,newVnode);
用新节点,替代旧节点
只会重新渲染新旧节点中有差异的部分,不会重新渲染整个节点。
- 销毁视图【删】
patch(newVnode,null);
用 null 替代目标节点
【核心】diff 算法
用于计算出 vDom 的最小变更(即比较出新旧 DOM 树的差异)
树 diff 的时间复杂度为 O(n^3)
- 遍历 tree1
- 遍历 tree2
- 排序
1000 个节点,要计算1亿次,算法不可用!
改用 diff 算法将时间复杂度降为 O(n)
- 只比较同一层级,不跨级比较
- tag 不相同,则直接删掉重建,不再深度比较
- tag 和 key,两者都相同,则认为是相同节点,不再深度比较
- 不使用key,则所有元素会先移除,再添加
若使用key,则若存在未改变的元素,只需进行移动即可。
若key 使用 index,则 key 的值为 0,1,2,3,4……,则若元素的顺序发生改变时,会出现问题。
相关的重要函数
- patchVnode
- addVnodes
- removeVnodes
- updatèChildren