1. 渲染项目列表时,“key” 属性的作用和重要性是什么?
key
的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。
如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。
而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。
2. 如何理解MVVM原理?
MVVM是一种软件架构模式,
通过轻量的contrl改变数据,vm层监听数据的变化更新dom树(JS对象),然后计算出最佳改变视图方法, 再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。
不直接改变视图,而是改变dom树,(使用Diff算法)将dom树映射成真实的DOM。
大大减少的渲染过程的消耗和提高了页面更新渲染速度。
不管mvc还是将mvvm最终目的都是将model层数据展示到view上。
MVC: Model(模型)、View(视图)、 Controller(控制器)。视图可以给控制器发指令,控制器改变/更新数据和视图。最终数据通过控制器展示到视图上。
JS操作真实DOM的代价!
用我们传统的开发模式MVC,原生JS或JQ操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。
在一次操作中,我需要更新10个DOM节点,浏览器收到第一个DOM请求后并不知道还有9次更新操作,因此会马上执行流程,最终执行10次。
例如,第一次计算完,紧接着下一个DOM更新请求,这个节点的坐标值就变了,前一次计算为无用功。计算DOM节点坐标值等都是白白浪费的性能。
即使计算机硬件一直在迭代更新,操作DOM的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户体验。
为什么需要虚拟DOM,它有什么好处?
Web界面由DOM树(树的意思是数据结构)来构建,当其中一部分发生变化时,其实就是对应某个DOM节点发生了变化,
虚拟DOM就是为了解决浏览器性能问题而被设计出来的。
如前,若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性attch到DOM树上,再进行后续操作,避免大量无谓的计算量。
所以,用JS对象模拟DOM节点的好处是,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。
真实DOM和其解析流程?
浏览器渲染引擎工作流程都差不多,大致分为5步,创建DOM树——创建StyleRules——创建Render树——布局Layout——绘制Painting
第一步,用HTML分析器,分析HTML元素,构建一颗DOM树(标记化和树构建)。
第二步,用CSS分析器,分析CSS文件和元素上的inline样式,生成页面的样式表。
第三步,将DOM树和样式表,关联起来,构建一颗Render树(这一过程又称为Attachment)。每个DOM节点都有attach方法,接受样式信息,返回一个render对象(又名renderer)。这些render对象最终会被构建成一颗Render树。
第四步,有了Render树,浏览器开始布局,为每个Render树上的节点确定一个在显示屏上出现的精确坐标。
第五步,Render树和节点显示坐标都有了,就调用每个节点paint方法,把它们绘制出来。
DOM树的构建是文档加载完成开始的?构建DOM数是一个渐进过程,为达到更好用户体验,渲染引擎会尽快将内容显示在屏幕上。它不必等到整个HTML文档解析完毕之后才开始构建render数和布局。
Render树是DOM树和CSSOM树构建完毕才开始构建的吗?这三个过程在实际进行的时候又不是完全独立,而是会有交叉。会造成一边加载,一遍解析,一遍渲染的工作现象。
CSS的解析是从右往左逆向解析的(从DOM树的下-上解析比上-下解析效率高),嵌套标签越多,解析越慢。
作者:LoveBugs_King
链接:https://www.jianshu.com/p/af0b398602bc
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
3.vue-Diff操作
当dom需改变时,先创新的虚拟dom, 和旧的虚拟dom进行平级比较(4种):
replace (替换):节点变
props(更新):属性/属性值变,
text(更新文本):文本内容变,
reorder(重排序):移动/增/删
得到一个diff结果,一组新旧虚拟dom差异数据。
以diff结果为根据,使用documentFragment (文档片段对象),对真实的dom进行一次更新。
reorder重排序(关于key)
当节点不使用key(唯一),再diff的平级比较中,按顺序比较,一旦出现差异,差异后同级的节点全部要被卸载,然后再按实际情况添加回去,如果差异出现的在前,后面顺序没问题,这种效率明显低下。
当节点使用key(唯一),再diff的平级比较中,它能够根据key,直接找到具体位置进行操作,效率比较高。
在实际代码中,会对新旧两棵树进行一个深度的遍历,每个节点都会有一个标记。每遍历到一个节点就把该节点和新的树进行对比,如果有差异就记录到一个对象中。
下面我们创建一棵新树,用于和之前的树进行比较,来看看Diff算法是怎么操作的。
old Tree new Tree平层Diff,只有以下4种情况:
1、节点类型变了,例如下图中的P变成了H3。我们将这个过程称之为REPLACE。直接将旧节点卸载并装载新节点。旧节点包括下面的子节点都将被卸载,如果新节点和旧节点仅仅是类型不同,但下面的所有子节点都一样时,这样做效率不高。但为了避免O(n^3)的时间复杂度,这样是值得的。这也提醒了开发者,应该避免无谓的节点类型的变化,例如运行时将div变成p没有意义。
2、节点类型一样,仅仅属性或属性值变了。我们将这个过程称之为PROPS。此时不会触发节点卸载和装载,而是节点更新。
查找不同属性方法3、文本变了,文本对也是一个Text Node,也比较简单,直接修改文字内容就行了,我们将这个过程称之为TEXT。
4、移动/增加/删除 子节点,我们将这个过程称之为REORDER。看一个例子,在A、B、C、D、E五个节点的B和C中的BC两个节点中间加入一个F节点。
例子我们简单粗暴的做法是遍历每一个新虚拟DOM的节点,与旧虚拟DOM对比相应节点对比,在旧DOM中是否存在,不同就卸载原来的按上新的。这样会对F后边每一个节点进行操作。卸载C,装载F,卸载D,装载C,卸载E,装载D,装载E。效率太低。
粗暴做法如果我们在JSX里为数组或枚举型元素增加上key后,它能够根据key,直接找到具体位置进行操作,效率比较高。常见的最小编辑距离问题,可以用Levenshtein Distance算法来实现,时间复杂度是O(M*N),但通常我们只要一些简单的移动就能满足需要,降低精确性,将时间复杂度降低到O(max(M,N))即可。
最终Diff出来的结果映射成真实DOM
虚拟DOM有了,Diff也有了,现在就可以将Diff应用到真实DOM上了。深度遍历DOM将Diff的内容更新进去。
根据Diff更新DOM 根据Diff更新DOM我们会有两个虚拟DOM(js对象,new/old进行比较diff),用户交互我们操作数据变化new虚拟DOM,old虚拟DOM会映射成实际DOM(js对象生成的DOM文档)通过DOM fragment操作给浏览器渲染。当修改new虚拟DOM,会把newDOM和oldDOM通过diff算法比较,得出diff结果数据表(用4种变换情况表示)。再把diff结果表通过DOMfragment更新到浏览器DOM中。
虚拟DOM的存在的意义?vdom 的真正意义是为了实现跨平台,服务端渲染,以及提供一个性能还算不错 Dom 更新策略。vdom 让整个 mvvm 框架灵活了起来
Diff算法只是为了虚拟DOM比较替换效率更高,通过Diff算法得到diff算法结果数据表(需要进行哪些操作记录表)。原本要操作的DOM在vue这边还是要操作的,只不过用到了js的DOMfragment来操作dom(统一计算出所有变化后统一更新一次DOM)进行浏览器DOM一次性更新。其实DOMfragment我们不用平时发开也能用,但是这样程序员写业务代码就用把DOM操作放到fragment里,这就是框架的价值,程序员才能专注于写业务代码。
作者:LoveBugs_King
链接:https://www.jianshu.com/p/af0b398602bc
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
4.Vue的响应式原理
当Compile解析dom检测到相关字符串 进行订阅者初始化,添加到dep(一个负责管理订阅者的对象)
observer监听变量,变量发生变化时,通知dep, dep遍历通知订阅者,订阅者调用对象的更新视图的函数(新建新的虚拟dom,然后比对旧的虚拟dom,diff算法,计算出更新视图的方法)更新视图。
observer监听器(监听数据变化并通过dep发布任务)
observer接受需要监听data,通过dep(每个属性一个dep)通知订阅者,
遍历data属性 Object.defineProperty给属性添加set和get方法,set方法被触发时,通过dep给订阅者发布任务。
dep订阅器(存储订阅者,发布任务)
Dep 扮演的角色是调度中心/订阅器,主要的作用就是收集观察者Watcher和通知观察者目标更新。
每个属性拥有自己的消息订阅器dep,用于存放所有订阅了该属性的观察者对象,
当数据发生改变时,会遍历观察者列表(dep.subs),通知所有的watch,让订阅者执行自己的update逻辑。
watcher订阅者(订阅任务并调用视图更新方法)
订阅任务并调用视图更新方法
complie解析器(解析dom结构)
遍历所有子节点,识别节点的值{{}}、绑定的事件v-on、绑定的属性值v-model。
为节点添加事件监听,
为带有v-model添属性的节点加input事件,并赋值给-model绑定的变量,
为节点的值绑定的变量、v-model定的变量 初始化为订阅者。
双向绑定如何实现:
1、我们需要一个方法来识别视图中哪个元素被设置了双向绑定。
2、我们需要监视视图和数据的变化。
3、我们需要将所有变化传播到绑定视图或者数据。
几种实现数据双向绑定的做法:1、发布者-订阅者模式(backbone.js)、脏值检查(angular.js)、数据劫持(vue.js)。
发布者-订阅者模式
data对象中变量作为models, 遍历(被双向绑定的对象)订阅/取消订阅 对应的任务,当models属性值发生变化时会触发发布对应任务, 订阅者收到任务通知,进行一系列处理并响应到视图层。
脏值检查
和上种方式类似,但在数据驱动视图变化,不是改完值手动触发set函数触发视图更新,而是通过setInterval()定时轮询检测数据变化触发set函数。
数据劫持
vue.js是通过数据劫持(object.defineProperty()的set和get)结合发布者-订阅者方式,在数据变动时发布消息给订阅者,触发相应回调监听。
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者。
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模版替换数据,以及绑定相应的更新函数。
3、实现一个watcher,做为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知。执行指令绑定的相应回调函数,从而更新视图。
4、mvvm入口函数,整合以上三者。
维护订阅器
订阅器维护一个订阅器,负责实例化订阅者,当初始化和更新时,调用相关函数。
Dep为一个构造函数,有subs数组储存订阅者,addSub和notify两个函数,addSub负责在初始化订阅者初始化时(当Compile解析dom检测到相关字符串 进行订阅者初始化)添加到订阅器中,notify负责在观察到数据更新时被触发 去调用订阅者的更新函数。
实现Observe
observObserver主要是对 对象属性 通过 defineProperty()进行监听,getter时帮助订阅者初始化时加入订阅器,setter时更新对象属性及通知订阅者调用函数更新订阅者值。
一个构造函数Observer,一个触发函数observe。
observe函数判断参数是否是对象,是的话实例化一个Observe对象(对象属性 遍历递归 时判断子属性值 是否是对象)。
Observe接受一个对象,有Walk、defineReactive两个函数,Walk负责遍历对象每个属性调用defineReactive。defineReactive负责递归每个对象属性 设置监听器。
Q/A
每次defineReactive都会new Dep()再在getter中初始化push订阅者,Dep中怎么会有所有订阅者?
其实只有一个订阅者,每次都会实例一个Dep,有多个Dep。
setter时循环订阅器中每个订阅者调用update函数,update函数做了什么事情?
监听器更新数据时触发的更新函数判断 new/old数据是否相同,不相同就把旧Value赋予新值,并在全局执行回调函数(传入新旧值)
实现订阅者
订阅者每个订阅者实例有4个对象属性,cb(监听器更新数据时触发的函数),vm(组件对象),exp(绑定的属性key),value(绑定的属性值)。run和get两个函数,run为监听器更新数据时触发的更新函数判断 new/old数据是否相同,不相同就把旧Value赋予新值,并在全局执行回调函数(传入新旧值)。get为初始化时把自己添加进订阅器Dep()中。
实现Compile
解析器主要作用是 遍历递归解析dom节点,解析到双向绑定的指令,将初始化的数据初始化到视图中,实例化订阅器并绑定更新函数。
第一部分Compile构造函数有3个属性,vm(全局环境),el(html最高节点),fragment用来存放dom节点(我们数据更新dom时需要多次操作dom,通过createDocumentFragment创建一个虚拟父节点fragment,把dom移入fragment进行操作,操作完了直接替换整个dom(一次性替换操作效率更高比一次次操作块70%)。
init()调用了nodeToFragment、compileElement、compile三个函数。
nodeToFragment,把dom塞入fragment虚拟父节点。
compileElement,遍历递归fragment中dom,判断是元素节点的话执行compile函数,是文本节点且有'{{}}'的话执行compileText函数。如果节点有子节点继续递归执行compileElement。
compile,对dom节点的属性节点进行遍历,若有"v-"相关字段属性name,若有":on"相关字段则绑定的是事件,执行compileEvent事件,否则执行compileMdole事件。
第二部分"{{}}"对应的compileText函数,负责初始化节点textContent数据,并新增一个订阅者。
"v-on:"对应的compileEvent函数,负责取得事件名和事件值 通过addEventListener监听函数触发执行对应事件。
"v-model"对应compileModel函数,负责初始化节点value数据,并新增一个订阅者,再通过对node.addEventListener('input', function(...))在input数据变化时实时改变对象数据
第三部分mvvm入口
入口我们把整个流程结合起来看一遍
入口构造函数,需要一个数据对象data,需要一个函数对象methods(当data中数据变化时调用)。
有一个proxyKeys函数,作用,在访问selfVue的属性时代理为selfVue.data属性(this.data.name = 'canfoo'我们可以用更简洁的方式 this.name = 'canfoo' ),也是通过遍历每个data属性为每个属性添加监听器object.defineProperty(),在get内把对this.key的访问替换成this.data.key的属性值来处理。
监听器observe对数据对象进行监听。
实例化compile对象,把节点传入,在compile会对dom节点进行遍历递归,处理3种情况。1、"{{}}",初始化节点texteContent数值,实例化一个订阅者。2、"v-model",初始化节点value数值,实例化一个订阅者,并监听input事件实时对数据更新。3、"v-on:"把对应事件名和methods中事件进行绑定监听addEventListener。
实例化订阅者Watcher,会在初始化时把自己添加进订阅器Dep(),在数据更新时会通过this.c.call()触发传进来的函数 处理数据。
所有事情处理好后执行mounted函数。
作者:LoveBugs_King
链接:https://www.jianshu.com/p/70b06d82ccfc
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
5.简易实现Object.defineProperty下的绑定
var data = { name: '' }; // Data Bindings Object.defineProperty(data, 'name', { get : function(){}, set : function(newValue){ // 页面响应处理 document.getElementById('name').innerText = newValue data.name = value }, enumerable : true, configurable : true }); // 页面DOM listener document.getElementById('name').onchange = function(e) { data.name = e.target.value; }