美文网首页
手写 Vue Router、手写响应式实现、虚拟 DOM 和 D

手写 Vue Router、手写响应式实现、虚拟 DOM 和 D

作者: 丽__ | 来源:发表于2022-02-23 16:17 被阅读0次

    Virtual DOM 的实现原理

    • 了解什么是虚拟DOM,以及虚拟DOM的作用
    • Snabbdom的基本使用
    • Snabbdom的源码解析
    一、什么是虚拟DOM ----Virtual DOM
    • 虚拟DOM是由普通的JS对象来描述DOM对象


      image.png
    二、 为什么要使用Virtual DOM
    • 前端开发初期,MVVM框架解决视图和状态同步问题
    • 模板引擎可以简化视图操作,没办法跟踪状态
    • 虚拟DOM跟踪状态变化
    • 参考GitHub上Virtual-dom的动机描述
      • 虚拟DOM可以维护程序上的状态,跟踪上一次的状态
      • 通过比较前后两次状态差异更新真实DOM

    虚拟DOM用来维护视图和状态的关系

    三、虚拟DOM的作用和虚拟DOM库
    • 虚拟DOM的作用

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

      • Snabbdom
      • Vue.js 2.x 内部使用的虚拟DOM就是改造的Snabbdom
      • 大约200 SLOC(Single line of code)
      • 通过模块可扩展
      • 源码使用TypeScript 开发
      • 最快的Virtual Dom 之一
      • virtual-dom
    四、 Snabbdom 的基本使用
    • 1、基本步骤:
      • 初始化项目目录并安装Parcel
    //创建项目目录
    md snabbdom-demo
    //进入项目目录
    cd snabbdom-demo 
    //创建package.json
    npm init -y
    //本地安装parcel
    npm install parcel-bundler -D
    
    
    • 配置package.json中的scripts
    "scripts:"{
      "dev":"parcel index.html --open",
      "build":"parcel build index.html"
    }
    
    • 创建目录结构
      • 根目录创建index.html,引入src目录中的文件
      • 在src中创建js文件来导入使用的snabbdom进行编码
    image.png image.png image.png
    //导入snabbdom
     npm install snabbdom@2.1.0
    
    
    import { init } from 'snabbdom/build/package/init'
    import { h } from 'snabbdom/build/package/h'
    
    const patch = init([])
    
    • 3、案例1
    import { init } from 'snabbdom/build/package/init'
    import { h } from 'snabbdom/build/package/h'
    
    const patch = init([])
    // 第一个参数:标签+选择器
    // 第二个参数:如果是字符串就是标签中的文本内容
    let vnode = h('div#container.cls', 'hello world')
    let app = document.querySelector('#app')
    // patch函数中的第一个参数:旧的VNode,也可以是DOM元素
    // 第二个参数:新的VNode
    // 返回新的VNode
    let oldVnode = patch(app, vnode)
    
    vnode = h('div#container.xxx','hello Snabbdom')
    patch(oldVnode,vnode)
    
    image.png
    image.png
    • 4、案例2
    import { init } from 'snabbdom/build/package/init'
    import { h } from 'snabbdom/build/package/h'
    
    const patch = init([])
    
    let vnode = h('div#container', [
      h('h1', 'hello Snabbdom'),
      h('p', '这是一个段落'),
    ])
    
    let app = document.querySelector('#app')
    
    let oldVnode = patch(app, vnode)
    
    setTimeout(() => {
      // 更改内容
      //   vnode = h('div#container', [h('h1', 'hello World'), h('p', 'hello P!')])
      //   patch(oldVnode, vnode)
    
      //清除div中的内容  h('!')-->生成空的节点
      patch(oldVnode, h('!'))
    }, 2000)
    
    
    五、 Snabbdom 模块的使用
    • 模块的作用
      • Snabbdom 的核心库并不能处理DOM元素的属性、样式、事件等,可以通过注册Snabbdom默认提供的模块来实现
      • Snabbdom 中的模块可以用来扩展Snabbdom的功能
      • Snabbdom中的模块的实现是通过注册全局的钩子来实现的
    • 官方提供的模块
      • attributes 设置元素属性 会处理布尔类型的属性
      • props 设置元素属性 不会处理布尔类型的属性
      • dataset 处理html5中的data-的自定义属性
      • class 用来切换类样式
      • style 用来设置行内样式,可以很容易设置过度动画
      • 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、注册模块
    const patch = init([styleModule, eventListenersModule])
    // 3、使用h()函数的地儿个参数传入模块中使用的数据(对象)
    let vnode = h('div', [
      h('h1', { style: { background: 'red' } }, 'hello World'),
      h('p', { on: { click: eventHandler } }, 'hello p'),
    ])
    
    function eventHandler() {
      console.log('点击了')
    }
    
    let app = document.querySelector('#app')
    patch(app, vnode)
    
    
    六、 Snabbdom 源码解析
    npm install
     
    npm run build  
    
    查看
    
    七、 h() 函数
    • h函数介绍
      • 作用:创建vNode 对象
      • vue中的h函数
      • h 函数最早见于hyperscript,使用JavaScript创建超文本
    //vue中的h函数
    new Vue({
      router,
      store,
      render:h => h(App)
    }).$mount('#app)
    
    • 函数重载
      • 概念:参数个数或参数类型不同的函数,重载的概念和参数相关,和返回值无关
      • JavaScript 中没有重载的概念
      • TypeScript中有重载,不过重载的实现还是通过代码调整参数
    //函数重载--参数个数
    function add(a:number,b:number){
      console.log(a+b);
    }
    function add(a:number,b:number,c:number){
      console.log(a+b+c);
    }
    add(1,2)
    add(1,2,3)
    
    //函数重载--参数类型
    function add(a:number,b:number){
      console.log(a+b);
    }
    function add(a:number,b:string){
      console.log(a+b);
    }
    add(1,2)
    add(1,'2')
    
    
    // 函数的重载
    export function h (sel: string): VNode
    export function h (sel: string, data: VNodeData | null): VNode
    export function h (sel: string, children: VNodeChildren): VNode
    export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
    export function h (sel: any, b?: any, c?: any): VNode {
      var data: VNodeData = {}
      var children: any
      var text: any
      var i: number
      // 处理参数,实现重载的机制
      if (c !== undefined) {
        // 处理三个参数的情况
        // sel data children/text
        if (b !== null) {
          data = b
        }
        if (is.array(c)) {
          children = c
          // 如果c是字符串或者数字
        } else if (is.primitive(c)) {
          text = c
          // 如果c 是VNode
        } else if (c && c.sel) {
          children = [c]
        }
      } else if (b !== undefined && b !== null) {
        if (is.array(b)) {
          children = b
        } else if (is.primitive(b)) {
          text = b
        } else if (b && b.sel) {
          children = [b]
        } else { data = b }
      }
      if (children !== undefined) {
        // 处理children中的原始值(string/number)
        for (i = 0; i < children.length; ++i) {
          // 如果child 是string/number,创建文本节点
          if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
        }
      }
      if (
        sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
        (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
      ) {
        // 如果是svg,添加命名空间
        addNS(data, children, sel)
      }
      // 返回VNode
      return vnode(sel, data, children, text, undefined)
    };
    
    
    八、 快捷键

    快速定位
    Alt + ←向左方向键
    ctrl+鼠标左键

    九、 VNode
    image.png
    十、 Patch 整体过程分析
    • patch(oldVnode,newVnode)
    • 把新节点中变化的内容渲染到真实的DOM,最后返回新节点作为下一次处理的旧节点
    • 对比新旧VNode是否相同节点(节点的key和sel相同)
    • 如果不是相同节点,删除之前的内容,重新渲染
    • 如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVnode 的text不同,直接更新文本内容
    • 如果有新的VNode有children,判断子节点是否有变化
    十一、patchVnode
    image.png
    十二、 Diff 算法
    • 虚拟DOM中的Differences算法

      • 查找两棵树每一个节点的差异


        image.png
    • Snabbdom 根据DOM的特点对传统的diff算法做了优化

      • DOM操作时候很少会跨级别操作节点
      • 只比较同级别的节点


        image.png
    • 对比子节点的具体过程,在对开始和结束节点比较的时候,共有四种情况

      • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
      • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
      • oldStartVnode / newEndVnode (旧开始节点 / 新结束节点)
      • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)
    image.png
    • 开始和结束节点

      • 如果新旧开始节点是sameVnode(key和sel相同)
        • 调用patchVnode()对比和更新节点
        • 把旧开始和新开始索引往后移动 oldStartIdx ++ / newStartIdx ++


          image.png
    • 旧开始节点 / 新结束节点

      • 调用patchVnode()对比和更新节点
      • 把oldStartVnode对应的DOM元素,移动到右边,更新索引


        image.png
    • 旧结束节点 / 新开始节点

      • 调用patchVnode()对比和更新节点
      • 把oldStartVnode对应的DOM元素,移动到左边,更新索引


        image.png
    • 非上述四种情况


      image.png
    • 循环结束

      • 当老节点的所有子节点先遍历完(oldStartIdx>oldEndIdx),循环结束
      • 当新节点的所有子节点先遍历完(newStartIdx>newEndIdx),循环结束
    • oldStartIdx > oldEndIdx

      • 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx)
        • 说明新节点有剩余,把剩余节点批量插入到右边


          image.png
    • newStartIdx >newEndIdx

      • 如果新节点的数组先遍历完(newStartIdx > newEndIdx)
        • 说明老节点有剩余,把剩余节点批量删除


          image.png

    相关文章

      网友评论

          本文标题:手写 Vue Router、手写响应式实现、虚拟 DOM 和 D

          本文链接:https://www.haomeiwen.com/subject/rovclrtx.html