FinClip+系列 | VUE前端开发框架核心原理

网友投稿 704 2022-11-22

程序框架有很多,都是支持前端JavaScript语言的,也是支持vue.js框架的。FinClip小程序是兼容各家平台的。所以在学习了框架使用之后的进阶就要熟悉框架的底层原理。

FinClip+系列 | VUE前端开发框架核心原理

除私有化版本外,FinClip现已推出SAAS版本,无需部署即可使用全部功能,每月有10000次免费发布调用,平台自带小程序流量统计,可根据实际用量灵活拓展,帮助企业以最低的价格实现商业化运行。

1、数据响应式

首先判断数据的类型,如果是基础数据类型,直接返回,如果已经有ob属性,表示已经是响应式的数据了,直接返回该数据。如果是对象就走第2步,如果是数组就走第3步

对象是通过Object.defineProperty,在getter里收集依赖,在setter里触发更新

数组是首先拷贝数组的原型,然后基于拷贝的原型改写(push,pop,unshift,shift,sort,reverse,splice)七个可以改变数组长度的方法,然后将改写后的原型赋给数组的隐式原型

对数组的隐式原型赋值后,还要观测数组的每一项,重复第一步

如果Object.defineProperty的setter里赋值,如果新赋的值是对象,也要进行观测

如果对数组的操作是有数据新增(push,unshift,splice),还需要观测数组新增的每一项,同第4步(这里Vue源码的实现是给每个响应式数据[对象和数组]新增了一个不可枚举的属性ob,它的作用有三,其一是用来判断数据是否已经是响应式的数据,如果是就不需再次观测,其二是属性ob是Observer类的一个实例,实例上有对数组每一项进行响应式处理的方法),其三是$set方法中,ob用来判断要设置属性的对象是不是响应式的对象,如果它本身就不是响应式对象,则该属性无需定义为响应式的属性

对象是在Object.defineProperty的getter里进行依赖的收集,在setter里触发更新。具体是通过观察者模式,每一个属性都有一个Dep类的实例,Dep.target有值即指向watcher的时候,在dep内收集watcher,并且在watcher内收集dep,dep和watcher是多对多的关系,因为一个组件会有多个属性,而watcher是组件级的,所以一个watcher可能对应多个dep,dep可能对应多个组件,组件内部的computed和watch都是watcher。

不管是根组件还是非根组件(函数),它们的data最终的值都是对象,所以只会在data最外层对象的某些属性值是数组,所以在Object.defineProperty的getter里对数组进行依赖收集,我们知道依赖的收集是调用dep类上收集依赖的方法,Vue的做法是在创建Observer类的实例的时候,定义了一个属性dep,dep是Dep类的实例。对于多维数组和数组新增的数据,Vue的做法是,在创建Observer类的实例的时候,设置了一个不可枚举的属性ob,它的值是Observer类的实例,所以我们在对多维数组进行依赖收集的时候,可以调用ob的dep的方法,对于数组新增的数据,调用ob上的方法对数组的每一项做数据响应式,并且调用ob.dep上的notify方法触发更新。

1.1、数据初始化的顺序:props->methods->data->computed->watch

如果data的层级过深会影响性能

对象有新增和删除属性没办法做数据的响应式处理(通过$set解决)

如果给对象的属性赋值为对象,也会对赋值后的对象进行响应式处理

1.2、data中数组的响应式处理是通过改写数组原型上的七个方法(push/pop/shift/unshift/sort/reverse/splice)

在重写数组原型之前,Vue给每个响应式数据新增了一个不可枚举的ob属性,这个属性指向了Observer实例,可以用来防止已经被响应式处理的数据反复被响应式处理,其次,响应式的数据可以通过ob获取到Observer实例的相关方法

对于数组的新增操作(push/unshift/splice),会对新增的数据也做响应式处理

通过索引修改数组内容和直接修改数组长度是观测不到的

2、Vue如何进行依赖收集的?

每个属性都有dep实例,dep实例用来收集它所依赖的watcher

在模板编译的时候,会取值触发依赖的收集

当属性发生变化时会触发watcher更新

3、Vue的更新粒度是组件级?

首先渲染watcher是组件级的。在初始化的时候,会调用_init方法,_init内部会调用$mount方法,$mount方法会调用mountComponent方法,mountComponent方法内部定义了updateComponent方法,updateComponent方法内部就是调用_update方法将vnode渲染成真实DOM,mountComponent方法会new一个渲染watcher,并把updateComponent传给渲染watcher,所以渲染watcher可以重新渲染DOM(试想一下,如果我们没有把更新DOM渲染的方法传递给watcher,更改数据后,我们需要手动去调用DOM渲染的方法;传递给watcher后,数据变化后,可以让watcher自动的去调用更新DOM渲染的方法)

在render函数生成vnode时,会判断是否是原生的HTML标签,如果不是原生HEML标签即是组件,会创建组件的vnode,子组件本质是VueComponent函数,VueComponent内部会调用_init方法,所以创建子组件vnode的时候,也会new一个渲染watcher,所以说渲染watcher是组件级的,也就是说Vue的更新粒度是组件级的

4、模板编译原理

注意一:我们平时开发中使用的是不带编译的Vue版本(runtime-only),所以在传入选项的时候是不能使用template的

注意二:我们.vue文件中的template是经过vue-loader处理的,vue-loader其实也是使用vue-template-compiler处理的

如果选项options里有render直接使用render,如果没有render看选项里有没有tempalte,如果有就用template,如果没有就看选项里有没有el,如果有template=document.querySelector(el),最后用compileToFunctions(tempalte)生成render

最终都是生成render函数,优先级是render>tempalte>el

模板编译的整体逻辑主要分为三个部分:第一步:将模板字符串转换成elementASTs(解析器)第二步:对AST进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)(进行新旧vnode对比的时候可以跳过静态节点)第三步:使用elementsASTs生成render函数代码字符串(代码生成器)

4.1、生成AST的过程

其实就是while循环里不断的通过正则匹配字符串,如果是匹配到是开始标签,就触发start钩子处理开始标签和属性,如果匹配到文本,就触发chars钩子处理文本,如果匹配到结束标签,就调用end钩子处理结束标签。处理完后就把模板中已经匹配到子串截取出来,一直这样循环操作,直到模板的字符串被截取成空串跳出while循环。

在匹配到开始标签后,就把开始标签压入栈中,匹配到结束标签就把栈顶元素出栈。第一个进栈的元素就是根节点,除了第一根元素外,其他元素在进栈之前,栈顶的元素就是该元素的父亲节点,所以可以维护元素之间的父子关系(入栈元素的parent是栈顶元素,该入栈元素是栈顶元素的儿子),当栈被清空之后,根节点就是生成的AST匹配到文本内容是没有子节点的,所以它直接作为栈顶元素的儿子即可。

4.2、解析器运行过程

AST是用JS中的对象来描述节点,一个对象代表一个节点,对象的属性用来保存节点所需的各种数据。

解析器内部分了好几个子解析器,比如HTML解析器,文本解析器,过滤器解析器。其中最主要的是HTML解析器,HTML解析器的作用就是解析HTML,它在解析的过程中会不断的触发各种钩子函数。这些钩子函数包括,开始标签钩子函数(start)、结束标签钩子函数(end),文本钩子函数(chars)和注释钩子函数(comment)。

实际上,模板解析的过程就是不断的调用钩子函数的过程,读取template,使用不同的正则表达式匹配到不同的内容,然后触发对应的钩子函数处理匹配到的字符串截取片段。比如比配到开始标签,触发start钩子函数,start钩子函数处理匹配到开始标签片段,生成一个标签节点添加到抽象语法树上。

HTML解析器解析HTML的过程就是循环(while循环)的过程,简单来说就是利用HTML模板字符串来循环,每轮循环都从HTML字符串中截取一小段字符串,重复以上过程,一直到HTML字符串被截取成一个空串结束循环,解析完毕。

在解析开始标签和结束标签是用栈来维护的,解析到开始标签就压入栈中,解析到结束标签,就从栈顶取出对应的开始标签的AST,栈顶的前一个开始标签就是该标签的父元素,然后就可以建立父子元素之间的关系。

文本解析器是对HTML解析器解析出来的文本进行二次加工。文本分为两种类型,一种是纯文本,一种是带变量的文本。HTML解析器在解析文本的时候,并不会区分是纯文本还是带变量的文本,如果是纯文本,不需要进行任何处理,带变量的文本需要文本解析器的进一步解析,因为带变量的文本在使用虚拟DOM进行渲染时,需要将变量替换成变量中的值。

文本解析器通过正则匹配出变量,把变量改写成_s(x)的形式添加到数组中

4.3、初始渲染原理

首先是生成render函数

vm._render函数生成虚拟DOMrender函数主要返回了这样的代码_c('div'{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world")))),所以需要定义_c,_v,_s这样的函数才能真正转换成虚拟DOM

vm._update方法将生成的虚拟DOM进行实例挂载update方法的核心是利用patch方法来渲染和更新视图,这里是初次渲染,patch方法的第一个参数是真实DOM,更新阶段第一个参数是oldVnode

5、Vue.mixin的使用场景和原理

Vue.mixin的作用就是抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化的时候会调用mergeOptions方法进行合并,对于不同的key(data,hooks,components...)有不同的合并策略。如果混入的数据和组件本身的数据有冲突,会采用“就近原则”,以组件本身的为准。

mixin有很多的缺陷:命名冲突,来源不清晰,依赖问题

6、nextTick在哪里使用?原理是什么?

nextTick可用于获取更新后的DOM

Vue的数据更新是异步的,会把所有的数据更新操作都放入任务队列中,然后在nextTick中去依次执行这些任务,nextTick是一个异步任务,采用的是优雅降级(Promise->MutationObserver->setImmediate->setTimeout)

7、watch原理

watch的使用方式,可以是对象,可以是函数,也可以是数组

不论是哪种使用方式,watch的每一个属性对应的函数(数组的使用方式,数组中的每一项(函数))都是一个用户watcher,其实现都是调用的$watch(vm,handler)

$watch方法的实现都是newWatcher(),只不过是options参数里标记了是用户自定义的watcher(options.user=true)

watch的属性对应的函数里有新值和旧值,我们是如何返回新值和旧值的呢?

newWatcher()的时候传递的是属性的key,我们要把它包装成一个函数(函数内部就是根据key取值),赋值给Watcher类的getter属性,在Watcher类实例化的时候,会调用一次get方法,我们就可以拿到它的值(取值同时会进行依赖收集)

在值更新后,会再次调用Watcher类的get方法获得新值

然后判断watcher的类型,如果是用户watcher,执行callback,把新值旧值传递给callback

watchapi不管是哪种使用方式,最终都是一个key,一个函数,对应一个userwatcher,每一个watcher都有一个getter方法,watchapi对应的getter方法是根据key来封装的,getter方法就是取key对应的数据,因为watcher在初始化的时候默认会调用一次getter,所以就拿到key对应的旧值了,取值也就进行了依赖收集,当key对应的数据改变了,watcher的getter方法会再次执行,这时就拿到了新值,然后调用key对应的回调函数,将新值和旧值传给它

8、computed原理

每个计算属性本质上也是一个用户watcher,在它取值的时候进行依赖收集,computed依赖的值改变后触发更新

计算属性的watcher在初始化的时候会有两个属性lazy和dirty

watcher在初始化的时候,会默认调用一次get方法,但是computed默认是不执行的,所以用lazy属性来标记是computedwatcher

computed是有缓存的,即依赖的值没有发生改变,多次获取,是不会多次调用watcher的get方法获取值的,所以用dirty属性来标记是否需要重新计算值,如果不需要计算,直接返回watcher的value,如果需要计算,再来调用get方法获取新的值,再返回watcher的value补充:什么时候dirty的值是true呢?

computedwatcher初始化的时候

computedwatcher依赖的值改变时(调用了computedwatcher的update方法,即可表示依赖的值改变了)

9、diff算法

Vue的diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归和双指针的方式

首先比对是否是相同的节点,如果不是删除旧的DOM,生成新的DOM插入

如果是相同的节点,比对更新属性

判断是否是文本节点,如果是,判断文本内容是否相同,不同更新文本内容

比对新旧子节点,如果只有新的有子节点,新增子节点插入;如果只有旧的有子节点,将元素的innerHTML置为空

如果新旧都有子节点,比对新旧子节点(采用双指针)

依次是头头、尾尾、头尾、尾头比较,没有匹配到,就乱序比对

乱序比对:建立旧的节点的映射表(key->index)

新的起始节点是否能在旧的映射表中找到,不能找到直接在旧的前面插入,如果找到,将映射表找到的旧的节点,移动到前面,并将该位置置为null

因为在乱序比对中,有将旧节点置为null的情况,所以在进行子节点比对前,先判断该节点是否为null,为null顺移

比对完之后如果新的节点还有,插入新的节点(插入的位置要判断是否在哪里插入),如果旧的节点还有,删除旧的节点(null的位置跳过)

学习框架技术是为了更好的开发,学习底层原理是为了让产品更好用,更好的让FinClip小程序在各家平台上更好的兼容和流畅使用。

本文首发于凡泰极客博客,作者:李丽强

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:FinClip 黑客马拉松正式开赛,码力集结,等你来战!
下一篇:echart 扇形图,玫瑰图配置说明
相关文章

 发表评论

暂时没有评论,来抢沙发吧~