app开发者平台在数字化时代的重要性与发展趋势解析
744
2022-11-22
小程序框架有很多,都是支持前端JavaScript语言的,也是支持vue.js框架的。FinClip小程序是兼容各家平台的。所以在学习了框架使用之后的进阶就要熟悉框架的底层原理。
除私有化版本外,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小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~