先声明一下,这篇文章仅为个人的见解,如有不对的地方还请多多包涵与指正!感谢
关于这个话题呢,其实我早就想聊了,只是一直给忘了,这段时间呢,我也一直在组织语言,在想应该从哪里开始谈起,大家应该也都听说过费曼学习法,大概就是如何判断你是真的懂了某一个知识点呢?就是你去给别人讲,把别人给讲懂了那么你才算是懂了,那么我现在也来尝试一下。
说起这个话题呢,我觉得还得从宏观的角度来聊起,纵观前端的历史长河,最初就是一个浏览器用来展示学术论文的好像,最多有个图片了不起了,这个时候,页面是没有交互的,我们只能看,不能有任何的操作,页面也比较简单,这其实也是web1.0时代。
随着浏览器慢慢普及,各位大佬们想着能不能给页面增加一些交互效果,你比如我要提交一个表单,要知道那个时候带宽是很珍贵的,想想一个场景,用户好不容易填完那么多输入框,最后一点击,浏览器把数据发送到了服务器,等了好几分钟,给我返回一个某个字段格式有问题,这是不是很恼火。那么就需要有那么一门语言,来专门在浏览器中执行,给用户实时反馈一些信息。这个时候js横空出世,把一些校验给前置了,这是不是就很舒服。
慢慢地,页面地交互丰富起来了,这个时候,js代码也就越来越复杂了,各种花式操作dom,让开发者们苦不堪言,就在这个时候呢,jq闪亮登场!当时还称霸了好几年呢。它主要解决了两个问题,一个是浏览器的兼容性问题,我们只需要调用jq的统一api,就能够在各种浏览器中随意运行,是不是很nice。另一个呢就是dom操作,虽然没有完全让开发者们不用关心如何去操作dom,但是呢。它提供了很多便利的api给我们,还有他的链式调用,简直就是减少代码的利器!
随着时间的流逝,出现了这么一种mvvm的思想,它的理念就是让开发者们使用数据去驱动视图,视图的更新只于数据相关,只要数据变了,视图就自动地根据数据来渲染,大家不要再需要去关心操作dom的事,这个脏活累活就让框架去管好了,如今这业务是越来越复杂,让开发者还去关心dom操作,这不折磨人吗?
这可是颠覆性地变更,要弄出这么一个框架出来,得从哪里入手呢?首先应该想到的是,需要有这么一个玩意,它能够监听到我们数据得变化,一旦变化,就会自动去做某一件事,代码里来说就是触发某一个函数。这在es5之前是不可能办到的,因为js从语言层面来讲是没有提供给我们这个能力的,但是在es5出来一个叫Object.defineProperty
的api之后,让这个玩意的实现从理论上来说有了可能。它可以对一个对象的属性访问于赋值进行拦截,那么我们就可以在这上面做文章了不是?
我们可以写这么一个函数,暂且就叫它render函数吧,这个函数体呢,里面就是给某一个元素的innerTHTML赋值,内容就是来自于某一个变量(存着一个对象) 的属性值。如下:
const obj = {
a: 1
}
function render() {
const div = document.getElementById('app')
div.innerHTML = `obj.a = ${obj.a}`
}
这个时候我们的render函数其实跟obj建立了一种关系,render函数依赖obj的一个属性,我们希望当obj的a属性值发生变化时,我们的render函数能够自动运行,乍一看,是不是就似乎实现了mvvm所说的理论,数据驱动视图,数据发生变化,视图自动更新!
这时我们把Object.defineProperty
搞进来试试
const obj = {
a: 1
}
let value= obj.a
Object.defineProperty(obj, 'a', {
get() {
// 虽然获取obj.a的时候这里我可以知道,但是这里我要做啥?
return value
},
set(val) {
value = val
// 虽然给obj.a赋值的时候我可以在这里知道,但是这里我要做啥?
}
})
function render() {
const div = document.getElementById('app')
div.innerHTML = `obj.a = ${obj.a}`
}
思考一波,我们可以给a属性弄一个小容器,这个容器用来保存依赖函数,当get函数触发,我们往这个容器里把正在用到a属性的函数给push进去,当get函数触发时,说明数据发生变化了,这个时候,我们把容器里面之前收集到的依赖函数一一拿出来进行调用。是不是就可以了呢?,在思考一下细节,是不是每一个用到a属性的函数,都需要被a的容器收集进去?不是吧,我们在methods中配置的那些方法,里面用到了这些数据,当我们给这些数据重新赋值的时候,并不需要将这个methods函数重新运行一遍!
也就是说我们得做点小操作,就是在我们真正需要收集的函数运行之前,用一个变量标记一下,运行完之后,将变量给清空掉,话不多说,直接上代码:
class Dep {
static activeEffect = null
constructor() {
this.subs = []
}
depend() {
if (Dep.activeEffect) this.subs.push(Dep.activeEffect)
}
notify() {
this.subs.forEach(sub => {
sub()
})
}
}
function Observer(data) {
for (const prop in data) {
// 给每一个属性分配一个小容器
let dep = new Dep()
let value = data[prop]
Object.defineProperty(data, prop, {
get() {
// 这里判断是不是需要收集正在用到该属性的函数
if (Dep.activeEffect) {
dep.depend()
}
return value
},
set(val) {
value = val
dep.notify()
}
})
}
}
const obj = {
a: 1,
b: 2
}
// 将obj这个普通的数据变成一个响应式数据
Observer(obj)
function effect(fn) {
Dep.activeEffect = fn
fn()
Dep.activeEffect = null
}
// 将render函数的运行统一交给专门处理副作用的函数去运行
effect(function render() {
const div = document.getElementById('app')
div.innerHTML = `obj.a = ${obj.a},obj.b = ${obj.b}`
})
其实这就是最最基本的响应式原理了,只不过vue里面还考虑了很多边界条件和性能优化。
我们在想想,如果是这样写的话,是不是太费劲了,如果我们的项目很复杂,肯定是不能这么写,元素这么多,这模板字符串拼接不得累死。那么vue呢非常贴心地给我们发明了一种类似于html书写方式的模板,我们在模板中描述数据与视图的映射,然后vue提供的编译器,会将我们的模板转换成为render函数。其实这就涉及到了框架需要在可维护性和性能之间做一个权衡,我们知道直接操作dom肯定是比我们书写模板,然后去编译成render函数,再去找到前后的差异并只更新变化的地方去操作dom要效率更高的。后者最终还是会直接操作dom,而且前面还多出了这么多步骤。
因此vue选择了声明式(声明式就是我们书写的模板,告诉vue我渲染一个某某元素,这个元素有什么属性,要绑定什么事件,然后具体怎么做我不管)的方式让我们书写代码,即让我们的代码可维护性不那么差,又让我们的代码性能不那么差。为什么这么说呢?因为vue不是说编译出来的render函数里直接操作的dom,而是借鉴了react中的虚拟dom,所谓的虚拟dom,本质就是js中的一个普通对象,里面有一些属性来描述真实dom的一些信息。在每次需要重新渲染时,render函数不是说直接去操作原生的dom进行更新,而是根据最新的数据生成一颗最新的虚拟dom,然后拿到之前的旧的虚拟dom,将两个虚拟dom进行对比,找到真正不同的地方,然后再去做真实dom的更新。这就让我们的代码可维护性更强时候,性能损失最小化。
其实我们应该感觉得到,如果采用命令式书写代码,我们式需要手动去维护dom得创建更新啥的,这也不符合mvvm得理念。而我们如果使用声明式,则只是在模板中展示给vue我们想要得结果是什么,至于应该怎么去做,不需要我们去关心,因为vue已经把这一系列的活都给我们做好了。并且引入了虚拟dom作为缓冲层,给我们的代码带来了更高的可维护性同时,性能也还过得去。
那么还有什么地方有可操作的空间呢?其实就是该如何设计出一种算法(我们将它称之为diff算法),来更高效的对比出两者之间的差异,毋庸置疑,肯定是越快越好。核心就是在对比新旧虚拟dom的子节点上。在vue2中使用的是双端diff,利用双指针,从两端向中间步步逼近,直到新或旧虚拟节点的后指针小于前指针为止。
其实在vue3中,利用最长递增子序列来找出最小差异的算法的效率更高,对于模板编译部分也是做了很多优化,真的可以说是优化到了极致,关于vue3的部分咱们以后在细说,现在也就浅聊一下,有那么个印象。
其实关于vue框架的一些要聊东西呢主要就是这些了,还有一些细节部分比如怎么避免同步多次改变数据导致很多次没必要的渲染啦,如何做一些依赖清除的工作,对于数组是如何侦测它的变化的,对于Object.defineProperty
的一些缺陷(无法侦测到对象属性的新增与删除),计算属性以及watch监听是如何实现的等等这些细节部分我觉得去源码中去体会更加地能够理解深刻,如果只是看一些文章而没有尝试去看源码的话,总会感觉里真相差那么一步,就是碰不到她,而且在面试中是永远没有底气的,一旦被深问,就会败下阵来,无法与面试官谈笑风生。