React vdom 具体实现和 VUE 不同;
Vue 3.0 重写了 vdom;
VNode
JS 模拟 DOM 结构(JS 计算速度更快,使用 JS 计算出最小变更);
每个 VNode 对应一个真实的 DOM 节点(包含渲染一棵树所需的全部信息,如元素标签、属性、文本内容、子节点等),用于记录组件状态的变化,并提供高效的更新机制;
当应用的状态变化时,会生成新的 VTree 与旧的 VTree 进行比较,找出最小的差异,然后仅更新对应的DOM部分。
VDom:
实现 vue 和 React 的基石,所有 VNode 的集合(页面上所有元素以对象形式保存于内存中)
理解
html
<div id="div1" class="container">
<p>title</p>
<ul style="font-size: 20px">
<li>hello</li>
</ul>
</div>
模拟
const vdom = {
tag: 'div',
props: {
className: 'container',
id: 'div1'
},
children: [
{
tag: 'p',
children: 'title' // 文本内容
},
{
tag: 'ul',
props: {
style: 'font-size:20px'
},
children: [
{
tag: 'li',
children: 'hello' // 文本内容
}
]
}
]
}
h 函数返回一个 VNode 工厂函数,执行函数返回一个对象(即 JS 模拟的 DOM 结构)
import { h, createApp } from 'vue';
// 定义一个简单的组件
const MyComponent = {
render() {
return h('div', { class: 'container' }, [
h('p', null, '这是一个段落'),
h('button', { onClick: () => alert('点击了按钮') }, '点击我')
]);
}
};
createApp(MyComponent).mount('#app');
h 函数
function h(sel,b,c) {
// do something 根据接受的参数做不同的处理
return vnode(sel, data, children, text, undefined)
}
VNode 函数
function vnode(sel, data, children, text, elm, key) {
let key = data === undefined ? undefined : data.key
return { sel, data, children, text, elm, key } // 返回一个模拟的DOM对象
}
`sel` 为 `tag` 标签,如 div p a span;
`data` - 属性 attr;
`children` - 子节点;
`elm` - vnode 对应 DOM 真实元素;
`text` - 文本;
`key` 用于 diff 算法对比新旧 dom 实现更新和重新渲染;
Vue 模板编译
以下 obj.c 输出结果为undefined
const obj = {
a: 100,
b: 200,
}
console.log(obj.a)
console.log(obj.b)
console.log(obj.c) // undefined
使用with能改变{}内自由变量的查找方式(当作obj的属性查找),缺陷会破坏作用域规则
with (obj) {
console.log(a)
console.log(b)
console.log(c) // 报错
}
vue-template-compile将模板编译成render函数即h,执行render返回vnode,执行patch将vDOM渲染成真实DOM;
const compiler = require("vue-template-compiler")
const template = `<p>{{message}}</p>`
const res = compiler.compile(template)
console.log(res.render) // 打印
编译结果
with(this){return _c('p',[_v(_s(message))])}
function installRenderHelpers (target) {
target._o = markOnce;
target._n = toNumber;
target._s = toString;
target._l = renderList;
target._t = renderSlot;
target._q = looseEqual;
target._i = looseIndexOf;
target._m = renderStatic;
target._f = resolveFilter;
target._k = checkKeyCodes;
target._b = bindObjectProps;
target._v = createTextVNode;
target._e = createEmptyVNode;
target._u = resolveScopedSlots;
target._g = bindObjectListeners;
target._d = bindDynamicKeys;
target._p = prependModifier;
}
> `this`指`const vm = new Vue()
> `_c` 为 `createElement` 即`h`函数
> `p` 为标签
> `_v` 对应`createTextVNode`:创建`text`节点子元素
> `_s`对应`toString`:创建`string`类型字符串
完整结果
with (this) {
return createElement('p', [createTextVNode(toString(message))])
// 等同于 h('p',{},message)
}
包含子集
const template = `
<div id="div1" class="container">
<img :src="imgUrl"/>
</div>
with (this) {
return _c(
"div", // div标签
{ staticClass: "container", attrs: { id: "div1" } }, // 属性
[
// 创建IMG标签
_c("img", { attrs: { src: imgUrl } }),
]
)
}
v-if
const template = `
<div>
<p v-if="flag === 'a'">A</p>
<p v-else>B</p>
</div>
`
with (this) {
return _c(
"div",
[flag === "a" ?
_c("p", [_v("A")]) :
_c("p", [_v("B")])]
)
}
v-for
const template = `
<ul>
<li v-for="item in list" :key="item.id">{{item.title}}</li>
</ul>
`
with (this) {
return _c(
'ul',
_l(list, function (item) {
return _c('li', { key: item.id }, [_v(_s(item.title))])
}),
0
)
}
event
const template = `
<button @click="clickHandler">submit</button>
`
with (this) {
return _c('button', { on: { click: clickHandler } }, [_v('submit')])
}
再来看看最早定义组件的方式
Vue.component('my-component', {
template: `<div id='box'>{{name}}</div> `,
data() {
return {
name: '张三'
}
}
})
<body>
<div id="root">
<my-component />
</div>
<script>
Vue.component('my-component', {
// template: `...`,
render(h) {
return h(
'div',
{
attrs: {
id: 'box'
}
},
'张三'
)
}
})
new Vue({
el: '#root'
})
</script>
</body>
Vue 渲染过程
渲染过程: 解析 vue 模板,生成 render 函数 --->触发响应式,监听 data 中的 getter setter, 执行 render 函数( h 函数 )生成 vnode ---> patch(elem,vnode) 渲染出真实Dom
更新过程: data 改变触发 setter ---> 重新执行 render 函数 ---> 生成 newVnode ---> patch(vnode,newVnode)
Vue 是异步渲染的,无论方法中对 data 改变多少次都会汇总成一次;
Component render function :将模板编译成 render 函数,
执行 render 函数渲染渲染出真实 dom 树结构,同时会 touch 到 data,即会触发 data 中的 getter,watcher 则会收集起来进行观察,当进行数据的更改时则会触发 setter并通知进行 re-render
React 模板编译
JSX 等同于 Vue 模板, Vue 模板不是 html,被编译成 React.createElement() 即 h / render
const elem = <div><p>text</p></div>
tag(标签)、props(属性)、children(子集)
var elem = React.createElement('div', null, React.createElement('p', null, 'text'))
React 组件渲染和更新过程
渲染过程:render() 生成 vnode ---> patch(elem, vnode)
setState(newState) ----> dirtyComponents ---> render() 生成 newVnode ----> patch(vnode, newVnode)
React 中的 patch 可拆分为两个阶段:reconciliation 阶段(执行 diff 算法,纯计算),commit 阶段(将 diff 结果渲染)
reconciliation:渲染一个 React 应用程序时,一个描述应用程序的节点树被生成并保存在内存中,然后将该树刷新到渲染环境,当应用程序更新时(通常为 setState),会生成一棵新树,新树与之前的树进行比较,以计算更新渲染应用程序所需的操作
commit:react fiber(react内部运行机制):将reconciliation阶段任务拆分,DOM需要渲染时暂停空闲时恢复即window.requestIdleCallback
Diff
通过新旧 vnode 对比,得出最小的更新范围,最终对需要更新的 dom 进行实际操作;
最开始会遍历 tree1 和 tree 2 继续比较,然后进行排序,导致时间复杂度到达 O^3,后来进行了优化将时间复杂度优化到了O(n),只比较同一层级,如果 tag 不相同直接删除重建,如果 tag 和 key都相同不再深度比较,而是直接进行一个新旧 vnode 的比较
patch 函数中新旧 vNode 对比的大致原理:
- 首选判断参数一是否是容器元素,如果是创建一个空 vnode 并关联
- 判断是否前后 vnode 相等,如果相等(tag 和 key 都相等)进行下一步对比,不相等删除重建。
children和 text 的对比,分为4个方向,旧的开始和旧的开始,旧的结束和旧的结束,旧的开始和新的结束,旧的结束和新的开始4个维度进行计算
- 如果
oldVnode含有children,newVnode为text:移除旧vnode,设置新text; - 如果
oldVnode为text,newVnode含有children:创建新vnode,移除旧text; - 如果
oldVnode为text,newVnode也为text:移除旧的text设置新text; - 如果
oldVnode含有children,newVnode也含有children:框架不同算法不同,共性都依赖于key进行计算;
为什么 key 不建议使用索引
遍历对象的每一个属性深度对比是非常浪费性能的,如果不指定 key 默认为 index 下标
假设现在有这样一段代码:
const users = [{ username: 'bob' }, { username: 'sue' }]
users.map((u, i) => <div key={i}>{u.username}</div>)
它会渲染出 DOM 树
<div key="1">bob</div>
<div key="2">sue</div>
如果对数据进行了 unshift
const users = [
{ username: "new-guy" },
{ username: "bob" },
{ username: "sue" },
];
DOM 树就会变成这样,注意 key 的变化
<div key="1">new-guy</div>
<div key="2">bob</div>
<div key="3">sue</div>
DOM 树的前后对比
<div key="1">bob</div> | <div key="1">new-guy</div>
<div key="2">sue</div> | <div key="2">bob</div>
| <div key="3">sue</div>
肉眼开头加了一个 new-guy 而已,但是由于 React 使用 key 值来识别变化,所以 React 认为的变化是:
bob -> new-guy
sue -> bob
添加 sue
非常消耗性能,如果我们一开始就给它指定一个合适的 key,React 认为的变化就变成:
| <div key="1">new-guy</div>
<div key="1">bob</div> | <div key="2">bob</div>
<div key="2">sue</div> | <div key="3">sue</div>