1. 声明式地描述UIVue.js 3是一个声明式UI框架编写前端页面涉及到以下内容1. DOM元素例如是div还是a标签。2. 属性乳a标签的href属性或idclass等通用属性。3. 事件如clickkeydown等。4. 元素的层级结构DOM树的层级结构既有子节点又有父节点。怎样声明式的描述上述内容Vue.js 3的解决方案是1. 使用与HTML标签一致的方式描述DOM元素如描述一个div标签时使用divdiv;2. 使用与HTML标签一致的方式描述属性如dividappdiv;3. 使用:或v-bind描述动态绑定的属性如div :iddynamicIddiv;4. 使用或v-on描述事件如div clickhandlerdiv;5. 使用与HTML标签一致的方式描述层级结构如一个具有span子节点的div标签divspanspandiv;可以看到每个内容都有与之对应的描述方式。用户不需要手写任何命令式代码。这就是声明式地描述UI。除了使用模板来声明式地描述UI外还可以javaScript对象来描述UIconst title{//标签名称 tag:h1, // 标签属性 props:{onclick: handler}, //子节点 children:[{tag:span}]}对应到Vue.js 模板h1 clickhandlerspan/span/h1使用javaScript对象描述UI比使用模板描述更灵活。letlevel3const title{tag:h${level }}可以看到当level 值改变时对应的标签名字也会在h1和h6之间变化。如果用模板描述则需要穷举h1 v-iflevel1/h1h2 v-iflevel2/h2h3 v-iflevel3/h3h4 v-iflevel4/h4h5 v-iflevel5/h5h6 v-iflevel6/h6使用javaScript对象描述UIde 方式就是虚拟DOM。在Vue.js组件中手写的渲染函数就是使用虚拟DOM来描述UI的。import{h}fromvueexportdefault{render(){retutn h(h1,{onClick: handler})//虚拟DOM}}h函数是一个辅助创建虚拟DOM的工具函数Vue.js会根据组件的render函数的返回值拿到虚拟DOM然后把组件内容渲染出来。虚拟DOM 就是用javaScript对象来描述真实的DOM 结构虚拟DOM本身就是一个javaScript对象没有特殊含义你完全可以自己设计虚拟DOM结构如使用tagName代替tag。2. 初识渲染器虚拟DOM怎样变为真实DOM并渲染到浏览器页面中呢这就是渲染器要做的工作将虚拟DOM渲染为真实DOM。声明一个虚拟DOMconst vnpde{tag:div, props:{onClick:()alert(hello)}, children:click me}编写一个渲染器将上面的虚拟DOM渲染为真实DOMfunctionrenderer(vnode,container){//使用vnode.tag作为标签名称床架DOM元素 const eldocument.createElement(vnode.tag)//遍历props将属性事件添加到DOM元素 for(const keyinvnode.props){if(/^on/.test(key)){//如果key以on开头说明它是事件 el.addEventListener(key.substr(2).toLowerCase(), //事件名称 onCleck---click vnode.props[key]//事件处理函数)}}//处理子节点 if(typeof vnode,childrenstring){//如果children 是字符串说明它是元素的文本子节点 el.appendChild(document.createTextNode(vnode.children))}elseif(Array,isArray(vnode.children)){//递归地调用renderer函数渲染子节点使用当前元素el作为挂载点 vnode.children.forEach(childrenderer(child,el))}//将元素添加到挂载下 container.appendChild(el)}renderer函数接收两个参数vnode虚拟DOM对象。container一个真实的DOM元素作为挂载点渲染器会把虚拟 DOM渲染到该挂载点下。渲染器renderer的实现思路1. 创建元素把vnodetag作为标签名称来创建DOM元素。2. 为元素添加属性和事件遍历vnode.props对象,如果key以on字符开头说明它是一个 事件把字符on截取掉后再调用toLowerCase函数将事件名称小写化最终得到合法的事件名称 例如onClick会变成click最后调用addEventListener绑定事件处理函数。4. 处理children如果children是一个数组就递归地调用renderer继续渲染注意此 时我们要把刚刚创建的元素作为挂载点父节点如果children是字符串。则使用 createTextNode函数创建一个文本节点并将其添加到新创建的元素内。这个渲染器函数仅做了创建节点的工作渲染器的精髓都在更新节点的阶段。对于渲染器来说它需要精确地找到vnode对象的变更点并且只更新变更的内容不需要再走一遍创建元素的流程。3. 组件的本质组件不是真实的DOM元素它是是一组DOM元素的封装这组DOM元素就是组件要渲染的内容因此我们可以用虚拟DOM来描述组件。因此可以定义一个函数来代表组件而函数返回值就代表组件要渲染的内容const MyComponentfunction(){return{tag:div, props:{onClick:()alert(hello)}, children:click me}}可以看到组件的返回值也是虚拟DOM它代表组件要渲染的内容。现在就可以用虚拟来描述组件了即让虚拟DOM对象中的tag属性来存储组件函数const vnode{tag: myComponent}此时tag: myComponent用来描述组件tag不再是标签名称而是组件函数未了支持渲染组件需要修改前面的renderer函数如下functionrenderer(vnode,container){if(typrof vnode,tagstring){//说明vnode描述的是标签元素 mountElement(vnode,container)}elseif(typeofvnode.tagfunction){//说明vnode描述的是组件 mountComponent(vnode,container)}}mountElement函数内容与rendere修改之前的内容完全一致functionmountElement(vnode,container){//使用vnode.tag作为标签名称床架DOM元素 const eldocument.createElement(vnode.tag)//遍历props将属性事件添加到DOM元素 for(const keyinvnode.props){if(/^on/.test(key)){//如果key以on开头说明它是事件 el.addEventListener(key.substr(2).toLowerCase(), //事件名称 onCleck---click vnode.props[key]//事件处理函数)}}//处理子节点 if(typeof vnode,childrenstring){//如果children 是字符串说明它是元素的文本子节点 el.appendChild(document.createTextNode(vnode.children))}elseif(Array,isArray(vnode.children)){//递归地调用renderer函数渲染子节点使用当前元素el作为挂载点 vnode.children.forEach(childrenderer(child,el))}//将元素添加到挂载下 container.appendChild(el)}mountComponent函数实现如下functionmountComponent(vnode,container){//调用组件函数获取组件要渲染的内容虚拟DOM const subtreevnode.tag()//递归调用renderer渲染subtree renderer(subtree,container)}可以看到首先调用vnode.tag函数返回虚拟DOM,这个虚拟DOM就是组件要渲染的内容我们称之为subtree既然subtree是虚拟DOM那么直接调用renderer函数完成渲染即可。组件不一定是函数形式的也可以用javaScript对象来表达组件//MyComponent 是一个对象 const MyComponent{render(){return{tag:div, props:{onClick:()alert(hello)}, children:click me}}}接着修改渲染器renderer和mountElement函数functionrenderer(vnode,container){if(typrof vnode,tagstring){//说明vnode描述的是标签元素 mountElement(vnode,container)}elseif(typeofvnode.tagobject){//如果是对象说明vnode描述的是组件 mountComponent(vnode,container)}}修改mountElement函数functionmountComponent(vnode,container){//vnode.tag是组件对象调用它的的render函数得到组件要渲染的内容虚拟DOM const subtreevnode.tag.render()//递归调用renderer渲染subtree renderer(subtree,container)}在上述代码中vnode.tag是表达组件的对象调用该对象的render函数得到组件要渲染的内容也就是虚拟DOM。4. 模板的工作原理模板的工作涉及到Vue.js框架中另一个重要的组成部分编译器。编译器和渲染器一样只是一段程序它的工作就是将模板编译为渲染函数。如一下模板div clickhandlerclick me/div对于编译器来说模板就是一个普通的字符串它会分析该字符串并生成一个功能与之相同的渲染函数render(){returnh(div,{onClick: handler},click me)}以.vue文件为例一个.vue文件就是一个组件如下所示templatediv clickhandlerclick me/div/templatescriptexportdefault{data(){/*... */}, methods:{handler:(){/*... */}}}/script其中标签里的内容就是模板内容编译器会把模板内容编译成渲染函数并添加到exportdefault{data(){/*... */}, methods:{handler:(){/*... */}},render(){returnh(div,{onClick: handler},click me)}}所以无论是使用模板还是手写渲染函数对于一个组件来说它要渲染的内容最终都是通过渲染函数产生的然后渲染器(renderer函数)再把渲染函数(render函数)返回的虚拟DOM渲染为真实的DOM这就是模板的工作原理也是Vue.js渲染页面的流程。5. Vue.js是由各个模块组成的有机整体组件的实现依赖于渲染器渲染器调用渲染函数生成虚拟DOM并最终把它变成屏幕上真实的DOM模板的编译依赖于编译器编译器将模板编译为渲染函数renderVue.js各个模块之间是相互关联互相制约的共同构成一个有机整体。下面以渲染器和编译器两个关键模块为例理解它们如何配合工作并实现性能提升。有以下模板dividfoo:classcls/div根据之前的介绍可知编译器会将这段代码编译为渲染函数render(){//为了效果更加直观这里没有使用h函数而是直接采用了虚拟DOM对象 //下面的代码等价于 //return h(div,{id:foo,class: cls})return{tag:div, props:{id:foo, class: cls}}}可以发现在这段代码中cls是一个变量它可能会发生变化。渲染器的工作之一是寻找并且只更新变化的内容。所以当cls值变化时渲染器会自动寻找变更点。这个“寻找”会花费一些力气。从编译器视角来看如果编译器有能力分析动态内容并在编译阶段将这些信息提取出来直接交给渲染器这样渲染器就不用寻找变更点了。Vue.js的实现方案如下以Vue.js模板来说它的模板是有特点的能一眼看出idfoo是静态数据永远不会变化:classcls是一个v-bind绑定它是会变化的。所以编译器能识别出哪些是静态属性哪些是动态属性在生成代码时完全可以附带这些信息render(){return{tag:div, props:{id:foo, class: cls}, patchFlag:1//假设数字1代表class是动态的}}虚拟DOM对象中多出一个patchFlag属性假设数字1代表class是动态的这样渲染器知道只有class属性会发生改变。对于渲染器来说相当于省去寻找变更点的工作量性能自燃就提升了。可以看到编译器和渲染器之间是存在信息交流的。它们互相配合使得性能进一步提升而它们之间交流的媒介就是虚拟DOM对象。虚拟DOM对象中会包含多种数据字段每个字段都代表一定的含义。6. 总结Vue.js是一个声明式框架声明式的好处在于它直接描述结果用户不关注过程。Vue.js采用模板的方式来描述UI,但它同样支持使用虚拟DOM来描述UI。虚拟DOM比模板更灵活模板比虚拟DOM更加直观。渲染器的作用是把虚拟DOM渲染为真实DOM元素。它的工作原理是递归地遍历虚拟DOM对象并调用原生DOM API来完成真实DOM元素的创建。渲染器的精髓在于更新它会通过Diff算法找出变更点并且只会更新需要更新的内容。组件是一组虚拟DOM元素的封装它可以是一个返回虚拟DOM的函数也可以是一个对象但这个对象下必须有一个函数用来产出组件要渲染的虚拟DOM渲染器在渲染组件时会现获取组件要渲染的内容即执行组件的渲染函数并得到其返回值虚拟DOM我们称之为subtree最后递归地调用渲染器将subtree渲染出来即可。Vue.js模板会被编译器的程序编译为渲染函数编译器渲染器都是Vue.js的核心组成部分它们共同构成一个有机整体不同模块之间互相配合进一步提升框架新能。