Vue响应式原理+Virtual DOM

952 阅读12分钟

为什么要学习原理部分的内容?

响应式原理

数据驱动

在学习 Vue.js 的过程中,我们经常看到三个概念:

  • 数据驱动
  • 数据响应式
  • 双向数据绑定

响应式的核心原理

Vue 2.x 版本与 Vue 3.x 版本的响应式实现有所不同,我们将分别讲解。

  • Vue 2.x 响应式基于 ES5 的 Object.defineProperty 实现。
    • 设置 data 后,遍历所有属性,转换为 Getter、Setter,从而在数据变化时进行视图更新等操作。

      Object.defineProperty(obj, 'gender', {
            value: '男', // 设置值
            writable: true, // 是否可写
            enumerable: true, // 是否可遍历
            configurable: true // 是否可以进行后续的配置
          })
      
      var genderValue = '男'
      Object.defineProperty(obj, 'gender', {
        get () {
          console.log('任意获取时需要的自定义操作')
          return genderValue
        },
        set (newValue) {
          console.log('任意设置时需要的自定义操作')
          genderValue = newValue
        }
      });
      

      cn.vuejs.org/images/data…

      vue 2 响应式原理:

       <div id="app">原始内容</div>
        <script>
          // 声明数据对象,模拟 Vue 实例的 data 属性
          let data = {
            msg: 'hello'
          }
          // 模拟 Vue 实例的对象
          let vm = {}
          // 通过数据劫持的方式,将 data 的属性设置为 getter/setter
          Object.defineProperty(vm, 'msg', {
            // 可遍历
            enumerable: true,
            // 可配置
            configurable: true,
            get () {
              console.log('访问了属性')
              return data.msg
            },
            set (newValue) {
              // 更新数据
              data.msg = newValue
              // 数据更改,更新视图中 DOM 元素的内容
              document.querySelector('#app').textContent = data.msg
            }
          });
        </script>
      
    • 上述版本只是雏形,问题如下:

      • 操作中只监听了一个属性,多个属性无法处理
      • 无法监听数组变化(Vue 中同样存在)
      • 无法处理属性也为对象的情况
      • 下面我们来进行改进
        <div id="app">原始内容</div>
        <script>
          // 声明数据对象,模拟 Vue 实例的 data 属性
          let data = {
            msg1: 'hello',
            msg2: 'world',
            arr: [1, 2, 3],
            obj: {
              name: 'jack',
              age: 18
            }
          }
          // 模拟 Vue 实例的对象
          let vm = {}
      
          // 封装为函数,用于对数据进行响应式处理
          const createReactive = (function () {
              // --- 添加数组方法支持 ---
            const arrMethodName = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
            // 用于存储处理结果的对象,准备替换掉数组实例的原型指针 __proto__
            const customProto = {}
            // 为了避免数组实例无法再使用其他的数组方法
            customProto.__proto__ = Array.prototype
            arrMethodName.forEach(method => {
              customProto[method] = function () {
              // 确保原始功能可以使用(this 为数组实例)
                const result = Array.prototype[method].apply(this, arguments)
                // 进行其他自定义功能设置,例如,更新视图
                document.querySelector('#app').textContent = this
                return result
              }
            })
      
            // 需要进行数据劫持的主体功能,也是递归时需要的功能
            return function (data, vm) {
              // 遍历被劫持对象的所有属性
              Object.keys(data).forEach(key => {
                // 检测是否为数组
                if (Array.isArray(data[key])) {
                  // 将当前数组实例的 __proto__ 更换为 customProto 即可
                  data[key].__proto__ = customProto
                } else if (typeof data[key] === 'object' && data[key] !== null) {
                  // 检测是否为对象,如果为对象,进行递归操作
                  vm[key] = {}
                  createReactive(data[key], vm[key])
                  return
                }
      
                // 通过数据劫持的方式,将 data 的属性设置为 getter/setter
                Object.defineProperty(vm, key, {
                  enumerable: true,
                  configurable: true,
                  get () {
                    console.log('访问了属性')
                    return data[key]
                  },
                  set (newValue) {
                    // 更新数据
                    data[key] = newValue
                    // 数据更改,更新视图中 DOM 元素的内容
                    document.querySelector('#app').textContent = data[key]
                  }
                })
              })
            }
      
          })()
      
          createReactive(data, vm);
        </script>
      
  • Vue 3.x 响应式基于 ES6 的 Proxy 实现。
    • Proxy 回顾
    Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
const p = new Proxy(target, handler)
 // target:要使用 `Proxy` 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
 // handler: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 `p` 的行为。
    <script>
        const data = {
          msg1: '内容',
          arr: [1, 2, 3],
          obj: {
            name: 'william',
            age: 18
          }
        }

        const p = new Proxy(data, {
          get (target, property, receiver) {
            console.log(target, property, receiver)
            return target[property]
          },
          set (target, property, value, receiver) {
            console.log(target, property, value, receiver)
            target[property] = value
          }
        });
  </script>
  • vue3响应式原理
 <div id="app">原始内容</div>
  <script>
    const data = {
      msg: 'hello',
      content: 'world',
      arr: [1, 2, 3],
      obj: {
        name: 'william',
        age: 18
      }
    }

    const vm = new Proxy(data, {
      get (target, key) {
        return target[key]
      },
      set (target, key, newValue) {
        // 数据更新
        target[key] = newValue
        // 视图更新
        document.querySelector('#app').textContent = target[key]
      }
    });
  </script>

相关设计模式

设计模式(design pattern)是针对软件设计中普遍存在的各种问题所提出的解决方案。

观察者模式

观察者模式(Observer pattern)指的是在对象间定义一个一对多(被观察者与多个观察者)的关联,当一个对象改变了状态,所有其他相关的对象会被通知并且自动刷新。

核心概念:

  • 观察者 Observer
  • 被观察者(观察目标)Subject

image.png

<script>
    // 被观察者 (观察目标)
    //   1 添加观察者
    //   2 通知所有观察者
    class Subject {
      constructor () {
        // 存储所有的观察者
        this.observers = []
      }
      // 添加观察者功能
      addObserver (observer) {
        // 检测传入的参数是否为 观察者 实例
        if (observer && observer.update) {
          this.observers.push(observer)
        }
      }
      // 通知所有观察者
      notify () {
        // 调用观察者列表中每个观察者的更新方法
        this.observers.forEach(observer => {
          observer.update()
        })
      }
    }

    // 观察者
    //   1 当观察目标发生状态变化时,进行"更新"
    class Observer {
      update () {
        console.log('事件发生了,进行相应的处理...')
      }
    }


    // 功能测试
    const subject = new Subject()
    const ob1 = new Observer()
    const ob2 = new Observer()

    // 将观察者添加给要观察的观察目标
    subject.addObserver(ob1)
    subject.addObserver(ob2)

    // 通知观察者进行操作(某些具体的场景下)
    subject.notify();

  </script>

发布-订阅模式

发布-订阅模式(Publish-subscribe pattern)可认为是为观察者模式解耦的进阶版本,特点如下:

  • 在发布者与订阅者之间添加消息中心,所有的消息均通过消息中心管理, 而发布者与订阅者不会直接联系,实现了两者的解耦。 核心概念:
  • 消息中心 Dep
  • 订阅者 Subscriber
  • 发布者 Publisher

image.png

  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
  <script>
    // 创建了一个 Vue 实例(消息中心)
    const eventBus = new Vue()

    // 注册事件(设置订阅者)
    eventBus.$on('dataChange', () => {
      console.log('事件处理功能1')
    })

    eventBus.$on('dataChange', () => {
      console.log('事件处理功能2')
    })

    // 触发事件(设置发布者)
    eventBus.$emit('dataChange');
  </script>

设计模式小结

观察者模式是由观察者与观察目标组成的,适合组件内操作。

  • 特性:特殊事件发生后,观察目标统一通知所有观察者。 发布/订阅模式是由发布者与订阅者以及消息中心组成,更加适合消息类型复杂的情况。
  • 特性:特殊事件发生,消息中心接到发布指令后,会根据事件类型给对 应的订阅者发送信息。

Vue 响应式原理模拟

整体分析

要模拟 Vue 实现响应式数据,首先我们观察一下 Vue 实例的结构,分析要实现哪些属性与功能。

image.png Vue

  • 目标:将 data 数据注入到 Vue 实例,便于方法内操作。 Observer(发布者)
  • 目标:数据劫持,监听数据变化,并在变化时通知 Dep Dep(消息中心)
  • 目标:存储订阅者以及管理消息的发送 Watcher(订阅者)
  • 目标:订阅数据变化,进行视图更新 Compiler
  • 目标:解析模板中的指令与插值表达式,并替换成相应的数据

Vue 类

• 功能:

  • 接收配置信息
  • 将 data 的属性转换成 Getter、Setter,并注入到 Vue 实例中。
  • *监听 data 中所有属性的变化,设置成响应式数据
  • *调用解析功能(解析模板内的插值表达式、指令等)

image.png

class Vue {
  constructor (options) {
    // 1 存储属性
    this.$options = options || {}
    this.$data = options.data || {}
    // 判断 el 值的类型,并进行相应处理
    const { el } = options
    this.$el = typeof el === 'string' ? document.querySelector(el) : el

    // 2 将 data 属性注入到 Vue 实例中
    _proxyData(this, this.$data)

    // *3. 创建 Observer 实例监视 data 的属性变化
    new Observer(this.$data)

    // *4. 调用 Compiler
    new Compiler(this)
  }
}

// 将 data 的属性注入到 Vue 实例
function _proxyData (target, data) {
  Object.keys(data).forEach(key => {
    Object.defineProperty(target, key, {
      enumerable: true,
      configurable: true,
      get () {
        return data[key]
      },
      set (newValue) {
        data[key] = newValue
      }
    })
  })
}

observer类

功能:

  • 通过数据劫持方式监视data中的属性变化,变化时通知消息中心Dep
  • 需要考虑data的属性也可能为对象,也要转换成响应式数据
class Observer {
  // 接收传入的对象,将这个对象的属性转换为 Getter/Setter
  constructor (data) {
    this.data = data
    // 遍历数据
    this.walk(data)
  }
  // 封装用于数据遍历的方法
  walk (data) {
    // 将遍历后的属性都转换为 Getter、Setter
    Object.keys(data).forEach(key => this.convert(key, data[key]))
  }
  // 封装用于将对象转换为响应式数据的方法
  convert (key, value) {
    defineReactive(this.data, key, value)
  }
}

// 用于为对象定义一个响应式的属性
function defineReactive (data, key, value) {
  // 创建消息中心
  const dep = new Dep()

  // 检测是否为对象,如果是,创建一个新的 Observer 实例进行管理
  observer(value)

  // 进行数据劫持
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get () {
      console.log('获取了属性')
      // * 在触发 Getter 时添加订阅者
      Dep.target && dep.addSub(Dep.target)
      return value
    },
    set (newValue) {
      console.log('设置了属性')
      if (newValue === value) return
      value = newValue
      observer(value)

      // * 数据变化时,通知消息中心
      dep.notify()
    }
  })
}

function observer (value) {
  if (typeof value === 'object' && value !== null) {
    return new Observer(value)
  }
}

Dep类

  • Dep是Dependency的简写,含义为“依赖”,指的是Dep用于收集与管理订阅者与发布者之间的依赖关系
  • 功能
    • *为每个数据收集对应的依赖,存储依赖
    • 添加并存储订阅者
    • 数据变化时,通知所有的观察者
class Dep {
  constructor () {
    // 存储订阅者
    this.subs = []
  }
  // 添加订阅者
  addSub (sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 通知订阅者的方法
  notify () {
    // 遍历订阅者,并执行更新功能即可
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

Watcher类

功能:

  • 实例化Watch时,往dep对象中添加自己
  • 当数据变化触发dep,dep通知所有对应的Watcher实例更新视图
class Watcher {
  constructor (vm, key, cb) {
    // 当前 Vue 实例
    this.vm = vm
    // 订阅的属性名
    this.key = key
    // 数据变化后,要执行的回调
    this.cb = cb

    // 触发 Getter 前,将当前订阅者实例存储给 Dep 类
    Dep.target = this
    // 记录属性更改之前的值,用于进行更新状态检测(导致了属性 Getter 的触发)
    this.oldValue = vm[key]
    // 操作完毕后清除 target,用于存储下一个 Watcher 实例
    Dep.target = null
  }
  // 封装数据变化时更新视图的功能
  update () {
    const newValue = this.vm[this.key]
    // 如果数据不变,无需更新
    if (newValue === this.oldValue) return
    // 数据改变,调用更新后的回调
    this.cb(newValue)
  }
}

Compiler类

这里使用的是dom,而vue使用的是virtual dom

功能:

  • 进行编译模板,并解析内部指令与插值表达式
  • 进行页面的首次渲染
  • 数据变化时,重新渲染视图
class Compiler {
  constructor (vm) {
    this.vm = vm
    this.el = vm.$el

    // 初始化模板编译方法
    this.compile(this.el)
  }
  // 基础模板方法
  compile (el) {
    const childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      // 检测节点类型(文本节点、元素节点)
      if (isTextNode(node)) {
        // 编译文本节点内容
        this.compileText(node)
      } else if (isElementNode(node)) {
        // 编译元素节点内容
        this.compileElement(node)
      }
      // 检测当前节点是否存在子节点
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }
  // 封装文本节点编译方法
  compileText (node) {
    const reg = /\{\{(.+?)\}\}/g
    // 去除内容中不必要的空格与换行
    const value = node.textContent.replace(/\s/g, '')
    // 声明数据存储多段文本
    const tokens = []
    // 记录已经操作过的位置的索引
    let lastIndex = 0
    // 记录当前提取内容的初始索引
    let index
    let result
    while (result = reg.exec(value)) {
      // 本次提取内容的初始索引
      index = result.index
      // 处理普通文本
      if (index > lastIndex) {
        // 将中间部分的内容存储到 tokens 中
        tokens.push(value.slice(lastIndex, index))
      }
      // 处理插值表达式内容(去除空格的操作可省略)
      const key = result[1].trim()
      // 根据 key 获取对应属性值,存储到 tokens
      tokens.push(this.vm[key])
      // 更新 lastIndex 为了获取后面的内容
      lastIndex = index + result[0].length
      // 创建订阅者 Watcher 实时订阅数据变化
      const pos = tokens.length - 1
      new Watcher(this.vm, key, newValue => {
        // 数据变化,修改 tokens 中的对应数据
        tokens[pos] = newValue
        node.textContent = tokens.join('')
      })
    }
    if (tokens.length) {
      // 页面初始渲染
      node.textContent = tokens.join('')
    }
  }
  // 封装元素节点处理方法
  compileElement (node) {
    // 获取属性节点
    Array.from(node.attributes).forEach(attr => {
      // 保存属性名称,并检测属性的功能
      let attrName = attr.name
      if (!isDirective(attrName)) return
      // 获取指令的具体名称
      attrName = attrName.slice(2)
      // 获取指令的值,代表响应式数据的名称
      let key = attr.value
      // 封装 update 方法,用于进行不同指令的功能分配
      this.update(node, key, attrName)
    })
  }
  // 用于进行指令分配的方法
  update (node, key, attrName) {
    // 名称处理
    let updateFn = this[attrName + 'Updater']
    // 检测并调用
    updateFn && updateFn.call(this, node, key, this.vm[key])
  }
  // v-text 处理
  textUpdater (node, key, value) {
    // 给元素设置内容
    node.textContent = value
    // 订阅数据变化
    new Watcher(this.vm, key, newValue => {
      node.textContent = newValue
    })
  }
  // v-model 处理 
  modelUpdater (node, key, value) {
    // 给元素设置数据
    node.value = value
    // 订阅数据变化
    new Watcher(this.vm, key, newValue => {
      node.value = newValue
    })
    // 监听 input 事件,实现双向绑定
    node.addEventListener('input', () => {
      this.vm[key] = node.value
    })
  }
}

// 判断节点是否为元素节点
function isElementNode (node) {
  return node.nodeType === 1
}

// 判断节点是否为文本节点
function isTextNode (node) {
  return node.nodeType === 3
}

// 判断属性名是否为指令
function isDirective (attrName) {
  return attrName.startsWith('v-')
}

功能回顾与总结

  • vue类
    • 把data的属性注入到Vue实例
    • 调用Observer实现数据响应式处理
    • 调用Compiler编译模板
  • Observer类
    • 将data的属性转换为Getter/Setter
    • 为Dep添加订阅者Watcher
    • 数据变化发送时通知Dep
  • Dep类
    • 收集依赖,添加订阅者(watcher)
    • 通知订阅者
  • Watcher类
    • 编译模板时创建订阅者,订阅数据变化
    • 接到Dep通知时,调用Compiler中的模板功能更新视图
  • Compiler
    • 编译模板,解析指令与插值表达式
    • 负责页面首次渲染与数据变化后重新渲染

Virtual DOM

课程目标:

  • 了解什么是虚拟DOM,以及虚拟DOM的作用
  • 了解如何使用Virtual DOM,Snabbdom的基本使用
  • Snabbdom的源码解析

什么是Virtual DOM

  • Virtual DOM(虚拟DOM),是由普通的js对象来描述DOM对象

image.png

  • 使用Virtual DOM来描述真实DOM

image.png

为什么要使用 Virtual DOM

  • 前端开发刀耕火种的时代
  • MVVM 框架解决视图和状态同步问题
  • 模板引擎可以简化视图操作,没办法跟踪状态
  • 虚拟 DOM 跟踪状态变化
  • 参考 github 上 virtual-dom 的动机描述
    • 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
    • 通过比较前后两次状态差异更新真实 DOM Version:0.9 StartHTML:0000000105 EndHTML:0000001513 StartFragment:0000000141 EndFragment:0000001473

虚拟 DOM 的作用

  • 维护视图和状态的关系
  • 复杂视图情况下提升渲染性能
  • 跨平台
    • 浏览器平台渲染
    • DOM原生应用(Weex/React Native)
    • 服务端渲染 SSR(Nuxt.js/Next.js)
    • 小程序(mpvue/uni-app)等

image.png

虚拟 DOM 库

  • Snabbdom
    • Vue.js 2.x 内部使用的虚拟 DOM 就是改造的 Snabbdom
    • 大约 200 SLOC (single line of code)
    • 通过模块可扩展
    • 源码使用 TypeScript 开发
    • 最快的 Virtual DOM 之一
  • virtual-dom Snabbdom的使用:
  1. 安装parcel打包工具

image.png

  1. 导入Snabbdom 官方文档:github.com/snabbdom/sn…
  • 安装 Snabbdom
    • npm install snabbdom@2.1.0
  • 导入 Snabbdom
    • Snabbdom 的两个核心函数 init 和 h()
    • init() 是一个高阶函数,返回 patch()
    • h() 返回虚拟节点 VNode,这个函数我们在使用 Vue.js 的时候见过
  • 官方文档中导入的方式

image.png

  • 实际导入的方式 • parcel/webpack 4 不支持 package.json 中的 exports 字段

image.png

import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'

// 1 通过 h 函数创建 VNode
let vNode = h('div#box.container', '新内容')

// 获取挂载元素
const dom = document.querySelector('#app')

// 2 通过 init 函数得到 patch 函数
const patch = init([])

// 3 通过 patch,将 vNode 渲染到 DOM
let oldVNode = patch(dom, vNode)

// 4 创建新的 VNode,更新给 oldVNode
vNode = h('p#text.abc', '这是p标签的内容')
patch(oldVNode, vNode)
  1. 包含子节点
import { h } from 'snabbdom/build/package/h'
import { init } from 'snabbdom/build/package/init'

const patch = init([])

// 创建包含子节点的 VNode
//   - 参数2的数组为子节点列表,内部就应该传入 vNode
let vNode = h('div#container', [
  h('h1', '标题文本'),
  h('p', '内容文本')
])

// 获取挂载元素
const dom = document.querySelector('#app')

// 渲染 vNode
const oldVNode = patch(dom, vNode)

// 清空操作更新页面,h('!')代表生成一个注释节点
patch(oldVNode, h('!'))
  1. Snabbdom模块相关内容
  • 模块的作用
    • Snabbdom 的核心库并不能处理 DOM 元素的属性/样式/事件等,可以通过注册 Snabbdom 默认提供的模块来实现
    • Snabbdom 中的模块可以用来扩展 Snabbdom的功能
    • Snabbdom 中的模块的实现是通过注册全局的钩子函数来实现的
  • 官方提供的模块
    • attributes
      • 设置 DOM 元素的属性,使用 setattribute()
      • 处理布尔类型的属性
    • props
      • 和 attributes 模块相似,设置 DOM 元素的属性 element[attr] = calue
      • 不处理布尔类型的属性
    • dataset
      • 设置 data-* 的自定义属性
    • class
      • 切换类样式
      • 注意:给元素设置样式是通过 sel 选择器
    • style
      • 设置行内样式,支持动画
      • delayed/remove/destroy
    • eventlisteners
      • 事件监听器模块
  • 模块使用步骤
    • 导入需要的模块
    • init() 中注册模块
    • h() 函数的第二个参数处使用模块
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'

// 1 导入模块(注意拼写,导入的名称不要拼错)
import { styleModule } from 'snabbdom/build/package/modules/style'
import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'

// 2 注册模块(为 patch 函数添加模块对应的能力)
const patch = init([
  styleModule,
  eventListenersModule
])

// 3 使用模块
let vNode = h('div#box', {
  style: {
    backgroundColor: 'green',
    height: '200px',
    width: '200px'
  }
}, [
  h('h1#title', {
    style: {
      color: '#fff'
    },
    on: {
      click () {
        console.log('点击了 h1 标签')
      }
    }
  }, '这是标题内容'),
  h('p', '这是内容文本')
])

const dom = document.getElementById('app')
patch(dom, vNode)

Snabbdom 源码解析

  • 如何学习源码

    • 宏观了解
    • 带着目标看源码
    • 看源码的过程要不求甚解
    • 调试
    • 参考资料
  • Snabbdom 的核心

    • init() 设置模块,创建 patch() 函数
    • 使用 h() 函数创建 JavaScript 对象(VNode)描述真实 DOM
    • patch() 比较新旧两个 VNode
    • 把变化的内容更新到真实 DOM 树
  • Snabbdom 源码

  • h 函数介绍

    • 作用:创建 VNode 对象
    • Vue 中的 h 函数 image.png
  • 函数重载

    • 参数个数或参数类型不同的函数
    • JavaScript 中没有重载的概念
    • TypeScript 中有重载,不过重载的实现还是通过代码调整参数
    • 函数重载-参数个数
    • 函数重载-参数类型
  • vnode函数

import { Hooks } from './hooks'
import { AttachData } from './helpers/attachto'
import { VNodeStyle } from './modules/style'
import { On } from './modules/eventlisteners'
import { Attrs } from './modules/attributes'
import { Classes } from './modules/class'
import { Props } from './modules/props'
import { Dataset } from './modules/dataset'
import { Hero } from './modules/hero'

export type Key = string | number

export interface VNode {
  sel: string | undefined
  data: VNodeData | undefined
  children: Array<VNode | string> | undefined
  elm: Node | undefined
  text: string | undefined
  key: Key | undefined
}

export interface VNodeData {
  props?: Props
  attrs?: Attrs
  class?: Classes
  style?: VNodeStyle
  dataset?: Dataset
  on?: On
  hero?: Hero
  attachData?: AttachData
  hook?: Hooks
  key?: Key
  ns?: string // for SVGs
  fn?: () => VNode // for thunks
  args?: any[] // for thunks
  [key: string]: any // for any other 3rd party module
}

export function vnode (sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined): VNode {
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key }
}
  • patch 整体过程分析
    • patch(oldVnode, newVnode)
    • 把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
    • 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
    • 如果不是相同节点,删除之前的内容,重新渲染
    • 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容
    • 如果新的 VNode 有 children,判断子节点是否有变化
  • init函数
    • 接收一个数组,数组中包含了模块的一些相关功能,生命周期函数的声明
    • 高阶函数,返回一个patch函数