diff算法
虚拟 dom
虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用的各种状态变化会作用于虚拟DOM,最终映射到DOM上。
优点
- 虚拟DOM轻量、快速:当它们发生变化时通过新旧虚拟DOM比对可以得到最小DOM操作量,从而提升性能
patch(vnode, h('div#app', obj.foo))
- 跨平台:将虚拟dom更新转换为不同运行时特殊操作实现跨平台
const patch = init([snabbdom_style.default])
patch(vnode, h('div#app', {style:{color:'red'}}, obj.foo))
- 兼容性:还可以加入兼容性代码增强操作的兼容性
必要性
vue 1.0中有细粒度的数据变化侦测,它是不需要虚拟DOM的,但是细粒度造成了大量开销,这对于大
型项目来说是不可接受的。因此,vue 2.0选择了中等粒度的解决方案,每一个组件一个watcher实例,
这样状态变化时只能通知到组件,再通过引入虚拟DOM去进行比对和渲染。
整体流程
mountComponent() core/instance/lifecycle.js
渲染、更新组件
// 定义更新函数
const updateComponent = () => {
// 实际调用是在lifeCycleMixin中定义的_update和renderMixin中定义的_render
vm._update(vm._render(), hydrating)
}
_render core/instance/render.js
生成虚拟dom
_update core\instance\lifecycle.js
update负责更新dom,转换vnode为dom
patch() platforms/web/runtime/index.js
__patch__是在平台特有代码中指定的
Vue.prototype.__patch__ = inBrowser ? patch : noop
代码实现
环境搭建
yarn init
yarn add webpack webpack-cli webpack-dev-server html-webpack-plugin
"dev": "webpack-dev-server --open --mode=development --hot",
"build": "webpack --mode=production"
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin'),
{ resolve } = require('path');
module.exports = {
entry: './src/js/index.js',
output: {
path: resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
devtool: 'source-map',
plugins: [
new HtmlWebpackPlugin({
template: resolve(__dirname, 'src/index.html')
})
],
devServer: {
static: './'
}
}
实现虚拟 dom
虚拟 dom js 形式
import { createElement, render, renderDOM } from './virtualDom'
const vDom = createElement(
'ul',
{class:'list',style:'color:#efef;width:500px;height:300px;background-color:brown;'},
[
createElement('li', { class:'item', 'data-index': 0 }, [
createElement('p', { class: 'text' }, ['第一个列表项'])
]),
createElement('li', { class:'item', 'data-index': 1 }, [
createElement('p', { class: 'text' }, [
createElement('span', { class: 'title' }, ['第2个列表项'])
])
]),
createElement('li', { class:'item', 'data-index': 2 }, [
createElement('p', { class: 'text' }, ['第3个列表项'])
])
]
)
const rDom = render(vDom)
renderDOM(rDom)
虚拟dom
virtualDom.js
import Element from './Element'
export function createElement(type, props, childrens) {
return new Element(type, props, childrens)
}
Element.js
class Element {
constructor(type, props, childrens) {
this.type = type
this.props = props
this.childrens = childrens
}
}
export default Element
到这里实现了 createElement 方法, 生成了虚拟 dom
下面实现 render:createElement => createElement(), 虚拟 dom 转真实 dom 的方法
virtualDom.js
export function render(vDom) {
const { type, props, childrens } = vDom
const dom = document.createElement(type)
Object.keys(props).forEach(key => {
switch(key) {
case 'value':
if(dom.tagName == 'INPUT' || dom.tagName == 'TEXTAREA') { // input、textarea 的value 设置方式和其他标签不同
dom.value = props[key]
} else {
dom.setAttribute(key, props[key])
}
default: dom.setAttribute(key, props[key])
}
})
childrens.map(item => {
item = item instanceof Element
?
render(item)
:
document.createTextNode(item)
dom.appendChild(item)
})
return dom
}
执行render 方法后得到
最后执行渲染真实 dom 操作
export function renderDOM (vDom) {
document.querySelector('#app').appendChild(vDom)
}
简单实现一个 diff 算法
首先我们先要获取一个补丁规则, 也就是 patches, 他是经过 oldVN 和 newVN 对比出来的。
例子:
// 补丁规则
const patches = {
0: [
{ // 修改属性
type: 'ATTR',
attr: { css: 'list-wrap' }
}
],
2: [
{ // 修改属性
type: 'ATTR',
attr: { css: 'title' }
}
],
3: [
{ // 修改文本
type: 'TEXT',
text: '今晚不喝酒'
}
],
6: [
{ // 删除操作
type: 'REMOVE',
index: 6
}
],
7: [
{ // 替换节点
type: 'REPLACE',
newNode: newNode
}
]
}
两个新旧 vnode 如下:
const vDom1 = createElement(
'ul',
{class:'list',test:6,style:'color:#efef;width:500px;height:300px;background-color:brown;'},
[
createElement('li', { class:'item', 'data-index': 0 }, [
createElement('p', { class: 'text' }, ['第一个列表项'])
]),
createElement('li', { class:'item', 'data-index': 1 }, [
createElement('p', { class: 'text' }, [
createElement('span', { class: 'title' }, ['第2个列表项'])
])
]),
createElement('li', { class:'item', 'data-index': 2 }, [
createElement('p', { class: 'text' }, ['第3个列表项'])
])
]
)
const vDom2 = createElement(
'ul',
{class:'list-wrap test','data-test':66,style:'color:#efef;width:501px;height:300px;background-color:brown;'},
[
createElement('li', { class:'item', 'data-index': 0 }, [
createElement('p', { class: 'title' }, ['今晚不喝酒'])
]),
createElement('li', { class:'item', 'data-index': 1 }, [
createElement('p', { class: 'text' }, [])
]),
createElement('div', { class:'item', 'data-index': 2 }, [
createElement('p', { class: 'text' }, ['第3个列表项'])
])
]
)
const patches = diffDom(vDom1, vDom2) // 进行 diff 对比
vDom1, vDom2 虚拟 dom 形式
diffDom.js
let patches = {}
,depth = 0;
function diffDom(oldDom, newDom) {
vNodeWalk(oldDom, newDom, depth)
return patches // 返回补丁
}
function vNodeWalk(oldVn, newVn, index) {
let vnPatch = []
if(!newVn) { // 如果 new virtual dom 不存在
vnPatch.push({ // 加入规则
type: 'remove',
index
})
}else if(typeof oldVn === 'string' && typeof newVn === 'string') { // 如果是文本
vnPatch.push({ // 加入规则
type: 'text',
text: newVn
})
}else if(oldVn.type === newVn.type){ // 如果是标签
const attrPatch = diffAttr(oldVn.props, newVn.props) // 处理得标签规则 ex: {class: 'list-wrap test', data-test: 66}
if(Object.keys(attrPatch).length > 0) {
vnPatch.push({ // 加入规则
type: 'attr',
attrs: attrPatch
})
}
// 如果type 存在,说明是标签,还要对比子元素
diffChild(oldVn.childrens, newVn.childrens) // 通层级对比,深度优先遍历
}else {
vnPatch.push({ // 加入规则
type: 'replace',
newNode: newVn
})
}
if(vnPatch.length > 0) {
patches[index] = vnPatch // 写入规则表
}
}
// 对比属性
function diffAttr(oldProps, newProps) {
let attrPath = {}
for(let key in oldProps) {
// 修改属性
if((newProps[key] || newProps[key] == 0) && oldProps[key] !== newProps[key]) {
attrPath[key] = newProps[key]
}
}
for(let key in newProps) {
// 增加属性
if(!oldProps[key] && oldProps[key] != 0) {
attrPath[key] = newProps[key]
}
}
return attrPath
}
// 对比 child
function diffChild(oc, nc) {
oc.map((c, i) => {
vNodeWalk(c, nc[i], ++depth) // 递归, 通层级对比,深度优先遍历
})
}
export default diffDom
最后处理得 补丁
更改 - 根据 patches
index.js
doPatch(rDom, patches)
** doPatch.js ** 这里是根据 真实dom和patches规则比对, 模拟更新视图
import { render } from './virtualDom'
import Element from './Element'
let _patches = {},
index = 0;
function doPatch(rDom, patches) {
_patches = patches
console.log(_patches)
rDomWalk(rDom)
}
function rDomWalk(rDom) {
const rnPatch = _patches[index ++],
childNodes = rDom.childNodes;
[...childNodes].map(child => { // 先往下遍历,然后执行handleDom, 所以这里是从下向上执行
rDomWalk(child)
});
// 节点和对应的 patched 补丁匹配,和路由嵌套原理差不多处理到一个规则表matched再去匹配
rnPatch && handleDom(rDom, rnPatch);
}
function handleDom(node, patch) {
patch.map(item => {
console.log(node, item)
switch(item.type){
case 'text':
node.textContent = item.text
break;
case 'attr':
Object.keys(item.attrs).forEach(key => {
item.attrs[key]
?
node.setAttribute(key, item.attrs[key])
:
node.removeAtrribute(key)
})
break;
case 'remove':
console.log(node, item)
node.parentNode.removeChild(node);
break;
case 'replace':
const newNode = item.newNode instanceof Element
? render(item.newNode)
: document.createTextNode(item.newNode)
node.parentNode.replaceChild(newNode, node)
break;
default:
break;
}
})
}
export default doPatch