Vue3 → vNode更新与引入Diff算法

550 阅读17分钟

前置知识

vNode回顾

vNode 简介

vNode表示虚拟节点Virtual DOM,是用js对象来描述真实的DOM,是将DOM的标签、属性、内容都变成了对象的属性,在vue中是将template模版编译描述成VNode,然后进行后续的操作实现真实DOM的挂载

  • 优点作用
    • 兼容性强,不受执行环境的影响
      • VNode是JS对象,不管是Node还是浏览器,都可以进行使用操作,从而获取了服务端渲染原生渲染手写渲染等能力
    • 减少DOM操作
      • 任何DOM操作都只使用VNode进行操作对比,只需要在最后一步挂载更新DOM,不需要频繁操作DOM,提升性能;
VNode怎么生成与存放什么信息

在vue源码中,VNode是通过一个构造函数生成的;在初始化完选项,解析完模版后就需要挂载DOM了,此时就需要生成VNode,然后根据VNode生成DOM然后挂载

  • VNode生成

    • 执行渲染函数,得到整个模版的VNode
    • 渲染函数执行后会返回VNode,渲染函数会绑定上下文对象
    • 渲染函数创建的VNode有两种
      • 一种是普通的标签节点-createElement-new VNode new VNode(tag, data, children, undefined, undefined, context);
      • 一种是组件-createComponent createComponent(Ctor, data, context, children, tag);}
  • 存放的信息 - 普通属性

    • data - 存储节点的属性,class,style等 - 存储绑定的事件 - 其他
    • elm
      • 真实DOM节点,在需要创建DOM的时候进行赋值,生成VNode的时候,并不存在真实DOM;
        vnode.elm = document.createElement(tag)
    • context - 渲染这个模版的上下文对象
      • template中的动态数据就是从这个context中获取,而context就是Vue实例
      • 如果是页面级别的,context就是本页面的实例
      • 如果是组件,context则是组件的实例
    • isStatic - 是否是静态节点
      • 当一个节点被标记成静态节点时,说明这个节点不用更新了,当数据变化时,可以忽略对比他,提高对比效率
  • 存放的信息 - 组件相关属性

    • parent
      • 是组件的外壳节点
      • 外壳通常是父组件和子组件的关联,用来保存一些父组件传给子组件的数据
    • ComponentInstance
      • 组件的实例,保存在此属性对象
    • ComponentOptions
      • 存储一些父子组件PY交易的证据,如props、slot、children、propsData、tag等 image.png

常规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中只是做了更新与初始化挂载的逻辑,初始化挂载逻辑不变,更新逻辑中对比了childrenprops,即patchChildren(preVnode,vnode,el,parentComponent,anchor)patchProps(el,oldProps,newProps)
    • patchChildren中是将各种新旧VNode的变化类型做了兼容处理,最后可以达到依赖的响应式数据变化了视图可以进行定点更新,避免不必要的更新,影响性能
      • patchChildren内部的对比逻辑有:Array→TextText→TextText→ArrayArray→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)
  • childrenarray,新childrentext
    • array内容卸载 + text 内容挂载
    • 将旧的children数组中的内容清空,然后修改childrentext类型,并赋值对应的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);
        }
    }
    
  • childrentext,新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);
        }
    }
    
  • childrentext,新childrenarray
    • 清空文本内容,然后修改childrenarray类型
    • 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);
        }
    }
    
  • childrenarray,新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)
    }
  }
}

文章推荐

实现vnode的更新逻辑,引出diff算法