目前几乎所有的小程序框架都支持前端JavaScript语言和vue.js框架。FinClip小程序兼容各种平台,所以在学习了框架的使用之后,还需要熟悉框架的底层原理。1、数据响应式首先判断数据的类型。如果是基本数据类型,则直接返回。如果已经有ob属性,说明已经是响应式数据,直接返回数据。如果是对象,则转步骤2。如果是数组,则转步骤3。对象是通过Object.defineProperty在getter中收集依赖,在setter中触发数组的更新。首先复制数组的原型,然后根据复制的原型重写(push、pop、unshift、shift、sort、reverse、splice)七个可以改变数组长度的方法,然后对重写的原型进行赋值到数组的隐式原型。给数组隐式原型赋值后,需要观察数组中的每一项,重复第一步。如果你在Object.defineProperty的setter中赋值,如果新赋值是一个对象,你也需要观察它。如果对数组的操作有新的数据(push、unshift、splice),也需要观察。添加到数组中的每一项与第4步相同(这里Vue源码的实现是为每一个响应式数据[对象和数组]添加一个不可枚举的属性ob,它有3个功能,一个是使用To判断数据是否已经是响应式数据,如果是就不用再观察了,二是属性ob是Observer类的一个实例,该实例中有一个对每一项进行响应式处理的方法数组),第三个是$set方法中,ob用于判断要设置的对象是否为响应式对象。如果不是响应式对象,则该属性不需要定义为响应式属性。对象定义在Object.defineProperty依赖集合的getter中,触发setter中的更新。具体来说,通过观察者模式,每个属性都有一个Dep类的实例。当Dep.target有值指向watcher时,watcher被收集到dep中,dep被收集到watcher中。dep和watcher是多对多的关系,因为一个组件有多个属性,而watcher是组件级的,所以一个watcher可能对应多个dep,dep可能对应多个组件,computed和watch都在组件内部是观察者。无论是根组件还是非根组件(函数),它们数据的最终值都是一个对象,所以只有数据最外层对象的一些属性值是数组,所以数组定义在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如果数据层级太深,会影响性能。对象有新增和删除的属性,没有办法对数据做响应式处理($set解决)如果把对象的属性赋值给对象,赋值的对象也会做响应式处理1.2.data中数组的响应式处理是通过重写数组原型上的七个方法(push/pop/shift/unshift/sort/reverse/splice)在重写数组原型之前,Vue为每个响应式添加了一个不可枚举的ob属性data,指向Observer实例,可以用来防止响应已经处理过的数据被重复响应其次,可以通过ob.Observer实例的相关方法获取响应数据。对于数组的新操作(push/unshift/splice),新数据也会进行响应式处理,可以通过索引修改数组内容,不能直接修改数组长度。2、Vue是如何进行依赖收集的?每个属性都有一个dep实例,dep实例用来收集它所依赖的watcher。当模板被编译时,它会取一个值来触发依赖项的收集。当属性发生变化时,会触发watcher更新。3、Vue的更新粒度是组件级别的吗?第一个渲染观察者是组件级别的。初始化时会调用_init方法,_init内部会调用$mount方法,$mount方法会调用mountComponent方法。updateComponent方法定义在mountComponent方法内部,在_update方法内部调用updateComponent方法将vnode渲染成真实的DOM。mountComponent方法会创建一个新的renderingwatcher,并将updateComponent传递给renderingwatcher,这样renderingwatcher就可以重新渲染DOM了(试想一下,如果我们没有把updateDOMrendering方法传递给watcher,在改变数据之后,我们需要手动调用DOMRendering方法;传递给watcher后,数据发生变化后,watcher可以自动调用更新DOM渲染的方法)当render函数生成一个vnode时,会判断它是否是一个原生HTML标签。如果不是原生HEML标签,是component,会创建组件的vnode,子组件的本质是VueComponent函数,在VueComponent内部会调用_init方法,所以在创建子组件vnode时,还会创建一个新的renderingwatcher,所以renderingwatcher是组件级别的,也就是说,Vue的更新粒度是组件级别的4.模板编译原理注1:我们一般使用的是Vue版本不编译(仅限运行时)在开发中,所以我们不能在传递选项时使用模板。注2:我们.vue文件中的template是由vue-loader处理的,而vue-loader实际上是由vue-template-compiler处理的。如果option选项中有render,直接使用render。如果没有render,查看选项中是否有tempalte。如果有,就用template,如果没有,检查option里有没有el,如果有template=document.querySelector(el),最后用compileToFunctions(tempalte)生成render,最后生成render函数,优先级是渲染>tempalte>el模板编译翻译的整体逻辑主要分为三部分:第一步:将模板字符串转换为元素AST(解析器)第二步:在AST上标记静态节点,主要用于虚拟DOM渲染优化(优化器)(静态节点可以比较新旧vnode时跳过)步骤3:使用元素AST生成渲染函数代码字符串(代码生成器)4.1。生成AST的过程其实就是在while循环中通过正则表达式不断匹配字符串,如果匹配到开始标签,则触发开始钩子处理开始标签和属性。如果匹配到文本,则触发chars钩子来处理文本。如果匹配到结束标签,则调用结束钩子处理结束标签,处理模板中的子字符串已经匹配到并被拦截,继续循环操作,直到模板字符串被拦截为空字符串,跳出while循环。匹配开始标签后,将开始标签入栈,匹配结束标签时,将栈顶元素弹出栈外。压入堆栈的第一个元素是根节点。除了第一个根元素,在其他元素入栈之前,栈顶元素是该元素的父节点,因此可以保持元素之间的父子关系(入栈的元素parent为栈顶元素,压入栈的元素为栈顶元素的儿子),清栈时,根节点为生成的AST匹配无文本内容子节点,所以直接作为栈顶元素的儿子即可。4.2.解析器AST的运行过程使用JS中的对象来描述节点,一个对象代表一个节点,对象的属性用于存储节点所需的各种数据。解析器内部有几个子解析器,如HTML解析器、文本解析器、过滤解析器。其中最重要的是HTML解析器。HTML解析器的作用就是解析HTML,解析过程中会不断触发各种钩子函数。这些钩子函数包括开始标签钩子函数(start)、结束标签钩子函数(end)、文本钩子函数(chars)和注释钩子函数(comment)。模板解析的过程其实就是不断调用钩子函数,读取模板,使用不同的正则表达式匹配不同的内容,然后触发相应的钩子函数,对匹配到的字符串截取片段进行处理的过程。例如匹配到开始标签,触发开始钩子函数,开始钩子函数处理匹配到开始标签的分片,生成标签节点添加到抽象语法树中。HTML解析器解析HTML的过程是一个循环(while循环)的过程。简单的说就是使用HTML模板字符串来循环。每一轮循环从HTML字符串中截取一个小字符串,重复上述过程,直到将HTML字符串截取成空字符串结束循环,解析完成。在解析开始标签和结束标签时,由一个栈来维护。当开始标记被解析时,它被压入堆栈。当解析完结束标签时,从栈顶取出对应开始标签的AST。栈顶的上一个开始标签就是该标签的父元素,然后就可以建立父子元素的关系了。文本解析器是对HTML解析器解析出的文本进行二次处理。文本有两种,一种是纯文本,一种是带有变量的文本。解析文本时,HTML解析器不区分纯文本和带变量的文本。如果是纯文本,则不需要处理。Textwithvariables需要文本解析器进一步分析,因为textwithvariables在使用虚拟DOM进行渲染时,需要将变量替换为变量中的值。文本解析器通过正则模式匹配变量,将变量重写为_s(x)的形式添加到数组中。4.3.初始渲染的原理是生成渲染函数vm._render函数生成虚拟DOM。render函数主要返回这样的代码"world")))),所以需要定义_c,_v,_s等函数真正转化为虚拟DOMvm._update方法来挂载生成的虚拟DOM实例。update方法的核心是使用patch方法来渲染和更新视图。这是第一个渲染。patch方法的第一个参数是realDOM,update阶段的第一个参数是oldVnode5,Vue.mixin的使用场景和原理。Vue.mixin的作用是提取公共业务逻辑。原理类似于“对象继承”。当组件初始化时,会调用mergeOptions方法进行合并。不同的键(数据、钩子、组件……)有不同的合并策略。如果混入的数据与组件本身的数据有冲突,将采用“就近原则”,以组件本身的数据为准。Mixin有很多缺陷:命名冲突、来源不明、依赖问题6、nextTick用在什么地方?原理是什么?nextTick可用于获取更新后的DOMVue。数据更新是异步的。所有的数据更新操作都会被放入任务队列中,然后这些任务会在nextTick中依次执行。nextTick是一个异步任务,采用优雅降级(Promise->MutationObserver->setImmediate->setTimeout)7.watch的原理watch的使用可以是对象,函数,数组。无论使用哪种方式,watch的每个属性都对应一个function(array数组中的每一项(function)都是一个userwatcher,其实现调用$watch(vm,handler)$watch方法实现为newWatcher(),也就是options参数是在自定义watcher(options.user=true)watch属性对应的函数中有新值和旧值的标记,我们如何返回newvalue和oldvalue?newWatcher()传给属性的key时,我们需要把它封装成一个函数(函数里面是根据key的值),赋值给的getter属性Watcher类,在Watcher类实例化的时候调用一次get方法,我们就可以得到它的值(值会和依赖同时收集),值更新后,Watcher类的get方法将再次调用以获取新值,然后watcher的类型将为ju危险的。如果是用户观察者,回调将被执行。无论使用哪种方式,将新值和旧值传递给callbackwatchapi,最终都是一个key,一个function,对应一个userwatcher,每个watcher都有一个getter方法,getter方法对应watchapi是根据key进行封装的,getter方法是获取key对应的数据,因为watcher在初始化的时候默认会调用一次getter,所以会获取到key对应的旧值,而该值将被依赖地收集。当key对应的数据发生变化时,会再次执行watcher的getter方法,此时会获取到新的值,然后然后调用key对应的回调函数,将新值和旧值传给它。8.computed原理每个computed属性本质上都是一个userwatcher,取值时会收集依赖,当computeddependencies的值发生变化后触发更新具有computed属性的watcher在初始化时会有两个属性lazy和dirtywatcher。初始化的时候默认会调用一次get方法,但是默认不执行computed,所以使用lazy属性来标记computedwatchercomputed被缓存。即依赖的值没有变化,如果多次获取,则不会多次调用watcher的get方法获取值,所以使用dirty属性来标记该值是否需要重新计算。如果不需要计算,直接返回watcher的值。如果需要计算,则调用get方法获取新值,然后返回watcher的值。补充:dirty的值什么时候为真?computedwatcher初始化时,computedwatcher依赖的值发生变化(调用computedwatcher的update方法表示依赖的值发生变化)9.diff算法Vue的diff算法是级别比较,不管跨层次比较内部使用深度递归和双指针先比较是否是同一个节点。如果不删除旧的DOM,则生成一个新的DOM以插入。如果是同一个节点,比较更新后的属性,判断是否是文本节点。如果是,判断文本内容是否相同,不同则更新文本内容,比较新旧子节点。如果只有新的子节点有子节点,则添加一个新的子节点插入;如果只有旧的子节点有子节点,则将元素的innerHTML设置为空。如果新旧子节点都有子节点,则按照头对头、尾对尾、头对尾、尾对头的顺序比较新旧子节点(使用双指针)比较。如果不匹配,则乱序比较:为旧节点创建映射表(key->index)新的起始节点能否在旧映射表中找到,如果找不到,直接插入旧的前面,如果找到,将映射表中找到的旧节点移到前面,并将位置设置为null,因为在乱序比较中,存在将旧节点设置为null的情况,所以在子节点比较之前,先判断节点是否为null,如果为null,比较完成后如果有新节点,则插入新节点(插入位置判断是否插入),如果旧节点还在存在,删除旧节点(nullpositionskip)学习框架技术是为了更好的发展,学习底层原理是为了让产品更好的使用,更好的兼容,流畅的使用finclip小程序在各个平台。如果你对小程序的设计、开发和使用有任何想法,欢迎登录FinClip.com寻找答案。如果您在使用过程中遇到任何问题,请与我们联系。
