前置知识
vNode回顾
vNode 简介
vNode表示
虚拟节点Virtual DOM,是用js对象来描述真实的DOM,是将DOM的标签、属性、内容都变成了对象的属性,在vue中是将template模版编译描述成VNode,然后进行后续的操作实现真实DOM的挂载
- 优点作用
- 兼容性强,不受执行环境的影响
- VNode是JS对象,不管是Node还是浏览器,都可以进行使用操作,从而获取了
服务端渲染、原生渲染、手写渲染等能力
- VNode是JS对象,不管是Node还是浏览器,都可以进行使用操作,从而获取了
- 减少DOM操作
- 任何DOM操作都只使用VNode进行操作对比,只需要在
最后一步挂载更新DOM,不需要频繁操作DOM,提升性能;
- 任何DOM操作都只使用VNode进行操作对比,只需要在
- 兼容性强,不受执行环境的影响
VNode怎么生成与存放什么信息
在vue源码中,VNode是通过一个
构造函数生成的;在初始化完选项,解析完模版后就需要挂载DOM了,此时就需要生成VNode,然后根据VNode生成DOM然后挂载
-
VNode生成
- 执行渲染函数,得到
整个模版的VNode - 渲染函数执行后会返回VNode,渲染函数会
绑定上下文对象 - 渲染函数创建的VNode有两种
- 一种是
普通的标签节点-createElement-new VNodenew VNode(tag, data, children, undefined, undefined, context); - 一种是
组件-createComponentcreateComponent(Ctor, data, context, children, tag);}
- 一种是
- 执行渲染函数,得到
-
存放的信息 -
普通属性- data - 存储节点的属性,class,style等 - 存储绑定的事件 - 其他
- elm
- 真实DOM节点,在需要
创建DOM的时候进行赋值,生成VNode的时候,并不存在真实DOM;
vnode.elm = document.createElement(tag)
- 真实DOM节点,在需要
- context - 渲染这个模版的
上下文对象- template中的动态数据就是从这个context中获取,而context就是Vue实例
- 如果是页面级别的,context就是本
页面的实例 - 如果是组件,context则是
组件的实例
- isStatic - 是否是
静态节点- 当一个节点被标记成静态节点时,说明这个节点
不用更新了,当数据变化时,可以忽略对比他,提高对比效率
- 当一个节点被标记成静态节点时,说明这个节点
-
存放的信息 -
组件相关属性- parent
- 是组件的
外壳节点 - 外壳通常是父组件和子组件的关联,用来保存一些父组件传给子组件的数据
- 是组件的
- ComponentInstance
- 组件的
实例,保存在此属性对象
- 组件的
- ComponentOptions
- 存储一些父子组件PY交易的证据,如props、slot、children、propsData、tag等
- 存储一些父子组件PY交易的证据,如props、slot、children、propsData、tag等
- parent
常规VNode更新
vue中的更新粒度是
组件级别的(最终还是在DOM标签级别上进行更新的),组件的数据变化只会影响当前组件的更新,但是在组件更新的过程中,也会对子组件做一定的检查,判断子组件是否也需要更新,并通过某种机制避免子组件重复更新;
更新元素的逻辑是:对于一个VNode,当其children发生变化时,应该使得
数据的变化引起视图的更新,其思路就是将渲染函数最为响应式数据的依赖,当响应式数据更新的时候重新渲染函数,使得视图更新;
在进行数据变化引起视图更新中,为了精确定位需要更新的位置,避免数据未变化的视图也跟着更新,Vue引入了Diff算法进行优化,例如其中的双端Diff算法处理children更新;
依赖收集渲染函数
依赖的数据虽然变化了,但是使用了响应式数据的渲染函数并没有和响应式数据进行Effect绑定,因此不会重新执行,所以可以将
渲染函数用Effect包裹即可实现;
代码实现 主要看mountComponent逻辑
function patch(preVnode,vnode, container, parentComponent,anchor) {
if(!vnode) return
// 处理组件 分为普通节点和封装的组件
const { type, shapeFlag } = vnode
// Fragment -> 只渲染 children
switch (type){
case Fragment:
processFragment(vnode, container, parentComponent,anchor);
break;
case Text:
processText(vnode, container);
break;
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(preVnode,vnode, container,parentComponent,anchor);
} else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
processComponent(vnode, container, parentComponent,anchor); //挂载组件
}
break
}
}
function processComponent(vnode: any, container: any, parentComponent,anchor) {
// 挂载组件
mountComponent(vnode, container, parentComponent,anchor);
}
function mountComponent(initialVNode: any, container, parentComponent,anchor) {
// 通过虚拟节点创建组件实例
const insatnce = createComponentInstance(initialVNode,parentComponent);
const { data } = insatnce.type
// 通过data函数获取原始数据,并调用reactive函数将其包装成响应式数据
// const state = reactive(data())
// 为了使得自身状态值发生变化时组件可以实现更新操作,需要将整个渲染任务放入到Effect中进行收集
effect(() => {
setupComponent(insatnce); //处理setup的信息 初始化props 初始化Slots等
setupRenderEffect(insatnce, initialVNode, container,anchor); // 首次调用App组件时会执行 并将render函数的this绑定为创建的代理对象
})
}
此时响应式数据变化时对应的渲染函数也就执行了,但是只是进行
重新渲染了新的视图,并没有达到更新指定视图的目的
区分组件是初始化还是更新状态,重构相关前置逻辑
只有当组件被
首次渲染时,应该是初始化状态,后续的再次调用渲染函数应该是更新逻辑了,因此可以通过添加标识的方式进行解决,即给实例添加一个isMounted属性,标记其是否被挂载过,挂载过则走更新逻辑,否则走初始化渲染逻辑;
更新的时候获取到新旧的VNode,因此在调用patch之前,先将新旧VNode提取出来,可以在组件实例上添加一个
subTree属性,用来记录当前组件的render方法返回的VNode;
subTree在初次挂载的时候进行赋值初始化,更新的时候赋值为新的VNode进行更新
- 在组件实例上添加
isMounted属性标记
export function createComponentInstance(vnode,parent) {
const component = {
// ...
isMounted: false, // 标识是否是初次加载还是后续依赖数据的更新操作
subTree: null, // 记录当前组件实例的render方法返回的vnode
// provides:{}, //常规的provide 无法实现跨级的父子组件provide和inject
provides:parent?parent.provides:{},
// 实现跨级父子组件之间的provide和inject
//相当于是一个容器 当调用 provide 的时候会往这个容器里存入数据 供子组件的数据读取provide 的数据
// ...
};
component.emit = emit.bind(null,component) as any
return component;
}
- 在调用渲染逻辑前先判断是否是初始化加载渲染
function setupRenderEffect(
insatnce: any,
initialVNode,
container,
anchor
) {
insatnce.update = effect(() => {
if(!insatnce.isMounted){ //初次挂载操作 初次调用时会进入 insatnce.isMounted 初次为undefined
// 收集依赖 在进行依赖数据更新时可以进行新旧虚拟节点的对比 - update/App.js -> 当响应式数据发生变化时 可以自动触发render的重新执行与渲染
// 根据VNode获取组件的选项对象
// const subTree = insatnce.vnode.render.call(state,state);
const { proxy } = insatnce
//指定instance中的this到当前节点 统一this 而非外部变化的实例等指向
const subTree = (insatnce.subTree = insatnce.render.call(proxy)); //获取到虚拟节点树
// insatnce.subTree保存下来供后续更新操作时获得初始化-更新前的虚拟节点 subTree
// 通过render获取到组件需要渲染的内容 即render函数返回的虚拟DOM
// 通过调用patch函数来挂载组件所需要描述的内容 即subTree
patch(null,subTree, container, insatnce,anchor); //patch会将新的subTree挂在到指定的container上 重复多次调用会多次挂载
// 所有的 subTree 都初始化结束
initialVNode.el = subTree.el // 存储根节点到VNode中 方便后续获取
insatnce.isMounted = true
} else { // 更新操作
console.log('更新逻辑========')
}
})
}
- 重构渲染逻辑 - 添加更新VNode的逻辑
- 即需要对
patch函数进行重构,之前的patch函数只负责处理重新渲染VNode,现在的patch函数需要进行新旧VNode的对比更新或初始化渲染逻辑 - 更新时获取到新旧VNode
- 需要在组件实例上添加旧VNode的标识,然后在组件初始化渲染的时候进行标识的赋值操作;→ 代码如上
- 在更新逻辑中,获取到新旧VNode,通过重构后的
patch函数进行更新逻辑
function setupRenderEffect( insatnce: any, initialVNode, container, anchor ) { insatnce.update = effect(() => { if(!insatnce.isMounted){ //初次挂载操作 初次调用时会进入 insatnce.isMounted 初次为undefined // ... } else { // 更新操作 const { proxy } = insatnce //指定instance中的this到当前节点 统一this 而非外部变化的实例等指向 const subTree = insatnce.render.call(proxy); //获取到虚拟节点树 const prevSubTree = insatnce.subTree insatnce.subTree = subTree // 保存当前的subTree 供后续更新操作获取到更新前的虚拟节点 - subTree patch(prevSubTree,subTree, container, insatnce,anchor); //patch会将新的subTree挂在到指定的container上 } }) } - 即需要对
重构patch
主要重构点
- 旧版
patch逻辑
function patch(vnode, container, parentComponent) {
// 处理组件 分为普通节点和封装的组件
const { type, shapeFlag } = vnode
// Fragment -> 只渲染 children
console.log(type,shapeFlag,'type,shapeFlag=========')
switch (type){
// case Fragment:
// processFragment(vnode, container, parentComponent);
// break;
case Text:
processText(vnode, container);
break;
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(vnode, container,parentComponent);
} else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
processComponent(vnode, container, parentComponent); //挂载组件
}
break
}
}
只接受一个节点,不能够处理新旧
vNode的对比更新逻辑,重构后需要添加新参数-新旧VNode
- 新版
patch逻辑- 入参新添加新旧
VNode - 初次挂载
VNode的时候旧VNode为null,后续更新的时候需要获取到旧保存的和新产生的VNode的值进行入参到patch中 - 此次更新只会影响到
processElement的相关挂载,其他的不受影响 processElement中只是做了更新与初始化挂载的逻辑,初始化挂载逻辑不变,更新逻辑中对比了children和props,即patchChildren(preVnode,vnode,el,parentComponent,anchor)和patchProps(el,oldProps,newProps)patchChildren中是将各种新旧VNode的变化类型做了兼容处理,最后可以达到依赖的响应式数据变化了视图可以进行定点更新,避免不必要的更新,影响性能patchChildren内部的对比逻辑有:Array→Text、Text→Text、Text→Array、Array→Array,其中最后一个实现比较复杂,涉及到了Diff算法(常规+双端Diff算法)
function patch(preVnode,vnode, container, parentComponent,anchor) { if(!vnode) return // 处理组件 分为普通节点和封装的组件 const { type, shapeFlag } = vnode // Fragment -> 只渲染 children switch (type){ case Fragment: processFragment(vnode, container, parentComponent,anchor); break; case Text: processText(vnode, container); break; default: if (shapeFlag & ShapeFlags.ELEMENT) { processElement(preVnode,vnode, container,parentComponent,anchor); } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { processComponent(vnode, container, parentComponent,anchor); //挂载组件 } break } } function setupRenderEffect( insatnce: any, initialVNode, container, anchor ) { insatnce.update = effect(() => { if(!insatnce.isMounted){ //初次挂载操作 初次调用时会进入 insatnce.isMounted 初次为undefined // 收集依赖 在进行依赖数据更新时可以进行新旧虚拟节点的对比 - update/App.js -> 当响应式数据发生变化时 可以自动触发render的重新执行与渲染 // 根据VNode获取组件的选项对象 // const subTree = insatnce.vnode.render.call(state,state); const { proxy } = insatnce //指定instance中的this到当前节点 统一this 而非外部变化的实例等指向 const subTree = (insatnce.subTree = insatnce.render.call(proxy)); //获取到虚拟节点树 // insatnce.subTree保存下来供后续更新操作时获得初始化-更新前的虚拟节点 subTree // 通过render获取到组件需要渲染的内容 即render函数返回的虚拟DOM // 通过调用patch函数来挂载组件所需要描述的内容 即subTree patch(null,subTree, container, insatnce,anchor); //patch会将新的subTree挂在到指定的container上 重复多次调用会多次挂载 // 所有的 subTree 都初始化结束 // subTree VNode经过 patch 后就变成了真实的 DOM,此时subTree.el指向了根DOM元素 initialVNode.el = subTree.el // 存储根节点到VNode中 方便后续获取根DOM对象 insatnce.isMounted = true // 初始化后及时标记为已挂载 } else { // 更新操作 const { proxy } = insatnce //指定instance中的this到当前节点 统一this 而非外部变化的实例等指向 const subTree = insatnce.render.call(proxy); //获取到虚拟节点树 - 新的 VNode const prevSubTree = insatnce.subTree //旧的VNode insatnce.subTree = subTree // 保存当前的subTree 供后续更新操作获取到更新前的虚拟节点 - subTree // 新的 vnode 要更新到组件实例的 subTree 属性 作为下一更新的旧 vnode patch(prevSubTree,subTree, container, insatnce,anchor); //patch会将新的subTree挂在到指定的container上 } }) } function processElement(preVnode:any,vnode: any, container: any, parentComponent,anchor) { if(preVnode){ // 更新element节点 patchElement(preVnode,vnode, container,parentComponent,anchor) } else { // 挂载element节点 mountElement(vnode, container, parentComponent,anchor) } }patchChildren内部复杂逻辑对比在js中,两个空对象字面量在进行逻辑判断的时候是不相等的,虽然都是空对象,但是由于不同内存地址的引用,会不相等;因此为了解决这个问题,可以创建一个空对象,然后在复制空对象的时候将该值进行引用赋值到oldProps和newProps上
//将各种新旧`VNode`的变化类型做了兼容处理,最后可以达到依赖的响应式数据变化了视图可以进行定点更新,避免不必要的更新,影响性能 function patchElement(preVnode:any,vnode: any, container: any,parentComponent,anchor){ const oldProps = preVnode.props || EMPTY_OBJ; const newProps = vnode.props || EMPTY_OBJ; // vnode.el = preVnode.el 下次更新的时候是没有el的 需要在首次的时候进行赋值操作 const el = (vnode.el = preVnode.el) patchChildren(preVnode,vnode,el,parentComponent,anchor) patchProps(el,oldProps,newProps) } - 入参新添加新旧
patchChildren 逻辑分析及对应测试用例
变更情况
- 总测试用例入口
// example/patchChildren/App.js
import { h, ref } from "../../lib/guide-mini-vue.esm.js";
import ArrayToText from "./ArrayToText.js"
import TextToText from "./TextToText.js"
import TextToArray from "./TextToArray.js"
import ArrayToArray from "./ArrayToArray.js"
export default {
name: "App",
setup(){},
render() {
return h("div",{tId:1},[
h("p",{},"主页"),
// h(ArrayToText)
// h(TextToText)
h(ArrayToArray)
])
}
}
// example/patchChildren/main.js
import { createApp } from "../../lib/guide-mini-vue.esm.js"
import App from "./App.js"
const rootComponent = document.querySelector("#app")
createApp(App).mount(rootComponent)
- 旧
children是array,新children是text- array内容卸载 + text 内容挂载
- 将旧的
children数组中的内容清空,然后修改children为text类型,并赋值对应的text内容即可 unmountChildren(preVnode.children)// 清空原始数组内容hostSetElementText(container,vnodeChildren)// 更改类型 赋值text内容- 测试用例
// example/patchChildren/ArrayToText.js import { h,ref } from "../../lib/guide-mini-vue.esm.js"; // import { ref, h } from "../" const nextChildren = "newChildren"; const prevChildren = [h("div",{},"A"),h("div",{},"B")] export default { name: "ArrayToText", setup() { const isChange = ref(false) window.isChange = isChange; return { isChange } }, render(){ const _this = this; return _this.isChange === true ? h("div",{},nextChildren): h("div",{},prevChildren); } } - 旧
children是text,新children也是text- 直接修改文本即可
hostSetElementText(container,vnodeChildren)//修改text内容 提前判断新旧是否相同- 测试用例
// example/patchChildren/TextToText.js import { h,ref } from "../../lib/guide-mini-vue.esm.js"; // import { ref, h } from "../" const nextChildren = "newChildren"; const prevChildren = "oldChildren"; export default { name: "TextToText", setup() { const isChange = ref(false) window.isChange = isChange; return { isChange } }, render(){ const _this = this; return _this.isChange === true ? h("div",{},nextChildren): h("div",{},prevChildren); } } - 旧
children是text,新children是array- 清空文本内容,然后修改
children为array类型 hostSetElementText(container,'')// 设置为空文本内容mountChildren(vnodeChildren,container,parentComponent,anchor)// 挂载组件内容到anchor- 测试用例
// example/patchChildren/TextToArray.js import { h,ref } from "../../lib/guide-mini-vue.esm.js"; // import { ref, h } from "../" const prevChildren = "newChildren"; const nextChildren = [h("div",{},"A"),h("div",{},"B")] export default { name: "TextToArray", setup() { const isChange = ref(false) window.isChange = isChange; return { isChange } }, render(){ const _this = this; return _this.isChange === true ? h("div",{},nextChildren): h("div",{},prevChildren); } } - 清空文本内容,然后修改
- 旧
children是array,新children也是array- 数组对比
patchKeyChildren(preChildren,vnodeChildren,container,parentComponent,anchor) - 测试用例
import { h,ref } from "../../lib/guide-mini-vue.esm.js"; const isChange = ref(false); // 1. 左侧的对比 // (a b) c // (a b) d e // const prevChildren = [ // h("p", { key: "A" }, "A"), // h("p", { key: "B" }, "B"), // h("p", { key: "C" }, "C"), // ]; // const nextChildren = [ // h("p", { key: "A" }, "A"), // h("p", { key: "B" }, "B"), // h("p", { key: "D" }, "D"), // h("p", { key: "E" }, "E"), // ]; // 2. 右侧的对比 // a (b c) // d e (b c) // const prevChildren = [ // h("p", { key: "A" }, "A"), // h("p", { key: "B" }, "B"), // h("p", { key: "C" }, "C"), // ]; // const nextChildren = [ // h("p", { key: "D" }, "D"), // h("p", { key: "E" }, "E"), // h("p", { key: "B" }, "B"), // h("p", { key: "C" }, "C"), // ]; // 3. 新的比老的长 // 创建新的 // 左侧 // (a b) // (a b) c // i = 2, e1 = 1, e2 = 2 // const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")]; // const nextChildren = [ // h("p", { key: "A" }, "A"), // h("p", { key: "B" }, "B"), // h("p", { key: "C" }, "C"), // ]; // 右侧 // (a b) // c (a b) // i = 0, e1 = -1, e2 = 0 // const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")]; // const nextChildren = [ // h("p", { key: "D" }, "D"), // h("p", { key: "C" }, "C"), // h("p", { key: "A" }, "A"), // h("p", { key: "B" }, "B"), // ]; // 4. 老的比新的长 // 删除老的 // 左侧 // (a b) c // (a b) // i = 2, e1 = 2, e2 = 1 // const prevChildren = [ // h("p", { key: "A" }, "A"), // h("p", { key: "B" }, "B"), // h("p", { key: "C" }, "C"), // ]; // const nextChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")]; // 右侧 // a (b c) // (b c) // i = 0, e1 = 0, e2 = -1 // const prevChildren = [ // h("p", { key: "A" }, "A"), // h("p", { key: "B" }, "B"), // h("p", { key: "C" }, "C"), // ]; // const nextChildren = [h("p", { key: "B" }, "B"), h("p", { key: "C" }, "C")]; // 5. 对比中间的部分 // 删除老的 (在老的里面存在,新的里面不存在) // 5.1 // a,b,(c,d),f,g // a,b,(e,c),f,g // D 节点在新的里面是没有的 - 需要删除掉 // C 节点 props 也发生了变化 // const prevChildren = [ // h("p", { key: "A" }, "A"), // h("p", { key: "B" }, "B"), // h("p", { key: "C", id: "c-prev" }, "C"), // h("p", { key: "D" }, "D"), // h("p", { key: "F" }, "F"), // h("p", { key: "G" }, "G"), // ]; // const nextChildren = [ // h("p", { key: "A" }, "A"), // h("p", { key: "B" }, "B"), // h("p", { key: "E" }, "E"), // h("p", { key: "C", id:"c-next" }, "C"), // h("p", { key: "F" }, "F"), // h("p", { key: "G" }, "G"), // ]; // 5.1.1 // a,b,(c,e,d),f,g // a,b,(e,c),f,g // 中间部分,老的比新的多, 那么多出来的直接就可以被干掉(优化删除逻辑) const prevChildren = [ h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B"), h("p", { key: "C", id: "c-prev" }, "C"), h("p", { key: "E" }, "E"), h("p", { key: "D" }, "D"), h("p", { key: "F" }, "F"), h("p", { key: "G" }, "G"), ]; const nextChildren = [ h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B"), h("p", { key: "E" }, "E"), h("p", { key: "C", id:"c-next" }, "C"), h("p", { key: "F" }, "F"), h("p", { key: "G" }, "G"), ]; // 2 移动 (节点存在于新的和老的里面,但是位置变了) // 2.1 // a,b,(c,d,e),f,g // a,b,(e,c,d),f,g // 最长子序列: [1,2] // const prevChildren = [ // h("p", { key: "A" }, "A"), // h("p", { key: "B" }, "B"), // h("p", { key: "C" }, "C"), // h("p", { key: "D" }, "D"), // h("p", { key: "E" }, "E"), // h("p", { key: "F" }, "F"), // h("p", { key: "G" }, "G"), // ]; // const nextChildren = [ // h("p", { key: "A" }, "A"), // h("p", { key: "B" }, "B"), // h("p", { key: "E" }, "E"), // h("p", { key: "C" }, "C"), // h("p", { key: "D" }, "D"), // h("p", { key: "F" }, "F"), // h("p", { key: "G" }, "G"), // ]; // 2.2 // a,b,(c,d,e,z),f,g // a,b,(d,c,y,e),f,g // 最长子序列: [1,3] // const prevChildren = [ // h("p", { key: "A" }, "A"), // h("p", { key: "B" }, "B"), // h("p", { key: "C" }, "C"), // h("p", { key: "D" }, "D"), // h("p", { key: "E" }, "E"), // h("p", { key: "Z" }, "Z"), // h("p", { key: "F" }, "F"), // h("p", { key: "G" }, "G"), // ]; // const nextChildren = [ // h("p", { key: "A" }, "A"), // h("p", { key: "B" }, "B"), // h("p", { key: "D" }, "D"), // h("p", { key: "C" }, "C"), // h("p", { key: "Y" }, "Y"), // h("p", { key: "E" }, "E"), // h("p", { key: "F" }, "F"), // h("p", { key: "G" }, "G"), // ]; // 3. 创建新的节点 // a,b,(c,e),f,g // a,b,(e,c,d),f,g // d 节点在老的节点中不存在,新的里面存在,所以需要创建 // const prevChildren = [ // h("p", { key: "A" }, "A"), // h("p", { key: "B" }, "B"), // h("p", { key: "C" }, "C"), // h("p", { key: "E" }, "E"), // h("p", { key: "F" }, "F"), // h("p", { key: "G" }, "G"), // ]; // const nextChildren = [ // h("p", { key: "A" }, "A"), // h("p", { key: "B" }, "B"), // h("p", { key: "E" }, "E"), // h("p", { key: "C" }, "C"), // h("p", { key: "D" }, "D"), // h("p", { key: "F" }, "F"), // h("p", { key: "G" }, "G"), // ]; export default { name: "ArrayToArray", setup() {}, render() { return h("div", {}, [ h( "button", { onClick: () => { isChange.value = !isChange.value; }, }, "测试子组件之间的 patch 逻辑" ), h("children", {}, isChange.value === true ? nextChildren : prevChildren), ]); }, }; - 数组对比
公共逻辑整合
- unmountChildren 清空children
function remove(child){
const parent = child.parentNode
if(parent){
parent.removeChild(child)
}
}
function setElementText(el,text){
el.textContent = text
}
export function createRenderer(options){
const {
remove:hostRemove,
setElementText: hostSetElementText,
patchProp:hostPatchProp,
} = options;
}
// 补全区域
function unmountChildren(children){
for (let index = 0; index < children.length; index++) {
const el = children[index].el;
hostRemove(el)
}
}
- hostSetElementText 设置Text内容
if(preChildren !== vnodeChildren){
hostSetElementText(container,vnodeChildren)
}
- hostPatchProp对比props逻辑
export function createRenderer(options){
const {
remove:hostRemove,
setElementText: hostSetElementText,
patchProp:hostPatchProp,
} = options;
}
function patchProp(el,key,prevProps,nextProps){
const isOn = (key:string) => /^on[A-Z]/.test(key)
if(isOn(key)){
const event = key.slice(2).toLowerCase()
el.addEventListener(event,nextProps)
} else {
if(nextProps === undefined || nextProps === null){
el.removeAttribute(key, nextProps)
} else {
el.setAttribute(key, nextProps)
}
}
}
patchChildren内部逻辑
function patchChildren(preVnode: any, vnode: any,container,parentComponent,anchor) {
const { shapeFlag } = vnode,
prevShapeFlag = preVnode.shapeFlag,
vnodeChildren = vnode.children,
preChildren = preVnode.children
if(shapeFlag & ShapeFlags.TEXT_CHILDREN){
// if(prevShapeFlag & ShapeFlags.ARRAY_CHILDREN){
// 情况1 老的是一个数组 新的是一个text
// 1、将老的 children 清空
// unmountChildren(preVnode.children)
// 2、设置 text
// hostSetElementText(container,vnodeChildren)
// }
// else {
// 情况1 老的是一个text 新的是一个text
// if(preChildren !== vnodeChildren){
// hostSetElementText(container,vnodeChildren)
// }
// }
if(prevShapeFlag & ShapeFlags.ARRAY_CHILDREN){
// 情况1 老的是一个数组 新的是一个text
// 1、将老的 children 清空
unmountChildren(preVnode.children)
// 2、设置 text
// hostSetElementText(container,vnodeChildren)
}
// 情况1 老的是一个text 新的是一个text
if(preChildren !== vnodeChildren){
hostSetElementText(container,vnodeChildren)
}
} else {
if(prevShapeFlag & ShapeFlags.TEXT_CHILDREN){
hostSetElementText(container,'')
mountChildren(vnodeChildren,container,parentComponent,anchor)
}else{
// 数组对比
// Array Diff Array
patchKeyChildren(preChildren,vnodeChildren,container,parentComponent,anchor)
}
}
}
patchProps内部逻辑
即更新props,这里的
patchProps函数就是在更新DOM节点的class、style、event以及其他的一些DOM属性
- 新props和旧props都存在,但是值不相同 -- 更新
- 新props的值为null或undefined,但是旧的props是存在的 -- 删除
- 新props直接key都不存在了 -- 删除
- hostPatchProp前面已实现
// src/shared/index.ts
export const EMPTY_OBJ = {};
// src/runtime-core/render.ts
function patchProps(el,oldProps,newProps){
if(oldProps === newProps) return
for (const key in newProps) {
const prevProp = oldProps[key]
const nextProp = newProps[key]
if(prevProp !== nextProp){
hostPatchProp(el,key,prevProp,nextProp)
}
}
if(oldProps === EMPTY_OBJ) return
for (const key in oldProps) {
// 遍历 oldProps 找出不存在于newProps中的key进行删除
if(!(key in newProps)){
hostPatchProp(el,key,oldProps[key],null)
}
}
}