什么是虚拟DOM
- Virtual DOM 是使用 JS 对象描述真实 DOM (普通的JS 对象,描述DOM 结构)
- Vue 中的 虚拟Dom 借鉴 Snabbdom, 并添加了Vue.js 的特性。(借鉴模块机制,钩子函数,diff算法等,添加例如指令,组件机制新特性)
为什么要使用 虚拟DOM
- 避免直接操作 DOM, 提高开发效率 (只关注业务代码实现,不需要关注 dom 浏览器兼容性问题)
- 作为一个中间层可以跨平台 (Web Weex移动端平台 SSR渲染)
虚拟 DOM 不一定可以提高性能
- 首次渲染时会增加开销 (需要维护一层额外的虚拟dom)
- 复杂视图情况下提升渲染性能 (频繁dom操作,通过diff算法,对比新旧两个虚拟dom树差异,并更新差异)
h函数
vm = new Vue({
el:"#app",
render(h){
// h(tag, data, children) // tag元素标签, data元素属性, children 数组则表示子元素,字符串表示元素内容
// return h('h1',this.msg) // 省略 data
// return h('h1', {domProps:{ innerHTML:this.msg }}) // dom属性
// return h('h1', {attrs:{ id: "title" }}, this.msg) // 标签属性和dom内容
let vnode = h('h1', {attrs:{ id: "title" }}, this.msg)
console.log(vnode)
return vnode;
},
data:{
msg:"hello world"
}
})
输出结果
方法总结 vm.$createElement(tag,data,children,normalizeChildren)
- tag 标签名或者组件对象
- data 描述tag,可以设置 DOM属性或者标签属性
- children 表示tag中的文本内容,或者子节点
返回js对象(vnode对象)总结
- tag:标签名,纯文本为undefined
- data:描述配置
- children:子节点
- text:文本
- elm:真实dom
- key:元素复用
- 其他
渲染位置
在 core/instance/lifecycle.js 中定义的
updateComponent = () => { // 这是个回调,wather里面会调用 // _render 或获取虚拟dom _update将虚拟dom转为真实dom vm._update(vm._render(), hydrating) }
解析 vm._render 方法
- 定位在 core/instance/render.js
此方法内部会获取用户传入的render,并执行,返回一个vnode对象
vnode = render.call(vm._renderProxy, vm.$createElement)
内部this执行 vm 实例,参数是一个函数
// 用户传递的render函数时调用 vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
- createElement方法中会对 传入的data做处理,进行铺平,然后调用 _createElement方法创建 VNode,并返回
解析 VNode创建 _createElement
- 内部对 tag 进行判断
- 带 is 为响应式组件赋值给tag
- 如果 children 是多维数组,进行拍平
- 最后创建 VNode 并返回
解析 vm._update
- 获取了 VNode 对象后,会通过 _update渲染挂载真实dom
- _update 定义在 core/instance/lifecycle.js
- 内部进行 首次渲染 和 数据更新操作
- 使用 vm.__patch__进行新老节点比较
解析 vm.__patch__ 方法
patch函数,定义在platforms/web/runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
- patch函数通过 createPatchFunction 方法生成(方法为高阶函数,柯里化函数)
- 此方法传入一个对象。对象内包含基本的模块和平台相关模块,例如 指令,ref,属性,样式,事件,以及 Snabbdom没有的 transition
解析 createPatchFunction 方法
- 定位在 core/vdom/patch.js
- 此函数类似于 Snabbdom中的init ,最终返回了patch函数,(函数返回函数,高阶)
- 源码 line 700 左右
解析 patch 函数
- 判断 新节点是否存在,不存在把对应的老节点移除
- 判断 老节点不存在,新节点是否存在,如果存在, 调用 createElm创建新VNode
- 判断 老节点存在,老节点是vnode还是真实 dom,
- 如果是 vnode 并且, oldVnode和vnode是相同节点,进行比较,通过 patchVnode
- 如果不是 vnode 是 dom,把 dom 转化成 vnode
- 获取 vnode 的父dom,确定 到时候 vnode要插入的位置
解析 patch函数中的两个方法 createElm 和 patchVnode
createElm
- 如果节点被渲染过并且还有子节点,就克隆一份
- 通过 createComponent 处理组件情况
判断标签,注释节点,文本节点的 vnode创建
- 1.标签:如果定义html不存在的标签,警告。否则创建 vnode对应 dom元素,通过 createElement,并设置 vnode 作用域。非 weex 平台,处理子dom,调用create 钩子,插入到 标签中
- 2.注释:调用 createComment 并插入
- 3.文本:调用 createTextNode 并插入
patchVnode
- 如果是 vnode不是 dom ,并且 新旧 vnode key tag相同,都有子节点那么 调用 patchVnode进行diff算法比较
- 方法内部会获取 新旧vnode 的子节点。
- 判断新老节点 是否有文本,是文本节点,则替换文本
新节点没有文本,
新老节点都有子节点
- 调用 updateCHildren 对比更新dom
新节点有子节点,老节点没有子节点。
- 检查新节点子节点是否有重复的 key
- 老节点是否有text,清空文本
- 把新节点下的子节点转化成 dom
老节点有子节点,新节点没有子节点。
- 移除老节点子节点,触发remove,destory钩子
- 老节点有 text 清空文本
解析 updateChildren
- 如果两个vnode 节点 都有子节点, 且 他们 key tag 相同,则对子节点进行 diff算法比较,最大限度重用
- 参考地址
key的作用
<div id="app">
<button @click="handler"></button>
<ul>
<li v-for="item in items">{{ item }}</li>
</ul>
</div>
new Vue({
el:"#app",
data:{
items:["a","b","c","d"]
},
methods:{
handler(){
this.items.splice(1,0,"x")
}
}
})
在未设置key 情况下
- 视图修改2,3,4项,新增第4项 ,更新三次dom,插入了一个dom,一共操作了四次dom
- abcd => axbc => axbcd
设置key 情况下
- 从先向后比较,比较第二项时,由于key 不一致,并不是 sameVnode
- 继续判断,从后向前比较,abcd和axbcd 后三项一样,倒数第二项不一样 a 和 x , 并不是 sameVnode
- 继续判断,老开始和新结束
- 继续判断,老结束和新开始
- 以上都不满足,老节点先遍历完成,把新增的节点addVnode 进dom树
- dom操作只有1次,就是添加dom那次
设置key 比不设置 key dom操作少很多
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。