• 手写vue -2 -- 虚拟DOM、diff算法


    手写vue - 虚拟DOM、diff算法

    面试题:

    1. 请简述Diff算法的执行过程

    diff 的过程就是调用 名为 patch 的函数, 比较新旧节点, 一边比较,一边给真实DOM打补丁(定点更新)。

    patch函数接受2个参数:oldVnode 和 vnode,分别代表 旧的节点 和 新的虚拟节点。

    这个函数会比较 oldVnode 和 vnode 是否 相同, 即 函数 sameVnode(oldVnode, vnode)

    1. 老节点oldVnode.nodeType存在,即 老节点是dom元素,则初始化: createElm(vnode),将新vnode=>dom, parentEl直接appendChild,完成初始化

    2. 老节点oldVnode.nodeType不存在,即新老vnode都是虚拟dom,则需要对新旧两个vnode树进行比较,增加删除操作

    3. 新老vnode树的顶节点是否是同一个节点(tag、key相同,即为同一个节点):

      • 同一个节点: 则进行props操作、children操作: 、

        • props操作:遍历新vnode树中的props,查看旧vnode树中是否存在该属性,不存在直接设置;存在,比较属性值是否相同,相同就不管,不相同,则重新设置属性值

        • children操作:

          ① 新vnode树的children的typeof如果是string,即,新节点是文本节点,查看旧节点是否是文本节点:如果旧节点是文本节点,且值与新节点的值不相同,则直接设置dom的contentText为新节点的children值;

          ② 新vnode树的children是数组,即,存在子节点: 查看旧vnode树的children:

          ​ a. 如果旧vnode树的children是文本,则,遍历新vnode的children,进行逐个createElm(child),并appendChild进去

          ​ b. 如果旧vnode树的children是数组,则更新子节点 updateChildren(el, oldch, newCh)

      • 不同节点: 则直接createElm(新vnode树),再拿到老节点的el,获取父元素parentEl,用replaceChild方法进行替换

    2. 既然Vue通过数据劫持可以精准探测数据变化,为什么还需要虚拟DOM进行diff检测差异?

    现代前端框架有2种方式侦测变化,一种是pull, 一种是push

    • pull:代表是React, React是如何侦测变化的呢?用setStateAPI显示更新,然后React会进行一层层的Virtual Dom Diff操作找出差异,然后Patch到DOM上,React从一开始就不知道到底是哪发生了变化,只是知道「有变化了」,然后再进行比较暴力的Diff操作查找「哪发生变化了」,另外一个代表就是Angular的脏检查操作。
    • push:Vue的响应式系统是push的代表。当Vue程序初始化时就会对数据data进行依赖收集,一旦数据发生变化,响应式系统就会立刻得知,因此Vue是一开始就知道是『在哪里发生了变化』,但这又产生一个问题,Vue的响应式系统通常绑定一个数据就需要一个Watcher,一旦我们的绑定细粒度过高就会产生大量的Watcher,这会带来内存及依赖追踪的开销,而细粒度锅底会无法精准侦测变化。因此,Vue的设计是选择中等细粒度的方案,在组件级别进行push侦测的方式,也就是那套响应式系统,一个组件一个Watcher,通常我们会第一时间侦测到发生变化的组件,然后在组件内部进行Virtual Dom Diff 获取更加具体的差异,而VDom-diff 则是pull操作,Vue是push+pull结合的方式进行变化侦测的。

    vue代码

    之前的《手写vue - 实现数据响应式、数据双向绑定和事件监听》中,有些许不足:

    • 直接模板 => dom,跳过了虚拟dom的生成和相关操作。
    • 没有VNode,每当data中的数据发生变化时,都会进行实时的更新,增加了程序的负担。
    • 每个key对应一个Watcher,当其中一个值发生变化时,都会遍历执行更新方法,在Vue2是每个组件实例对应一个Watcher,利用VNode和diff算法减少更新的次数,且是批量异步更新。

    改进:

    • 一个组件只有一个Watcher,从而减少更新方法的触发次数,降低性能消耗;

    • 增加Vnode的概念,利用我们的简单diff算法,不直接对模板中的真实dom进行操作。

    vue对象

    // 1. 一个组件一个Watcher
    // 2. diff算法
    // 流程梳理:
    // vue实例化  =>  触发this.$mount(el),$mount()挂载  
    // 		=>  创建一个Watcher实例➕将组件更新函数 updateComponent方法传入到Watcher中,等watcher需要update的时候,进行调用updateComponent方法  
    // 		=>  当视图需要更新的时候,调用updateComponent方法,该方法中调用reader方法来获取vnode树,执行_update()将vnode转为真实dom。 
    // 		=> _update()方法中调用__patch__方法用来进行diff算法
    class Vue {
      constructor(options) {
        this.$options = options
        this.$data = options.data()
        this.$mounted = options.mounted
    
        // 数据代理
        this.proxy(this.$data)
    
        // 数据响应式处理
        this.observe(this.$data)
    
        if (options.el) {
          this.$mount(options.el)
        }
    
        if (this.$mounted) {
          this.$mounted.call(this)
        }
      }
    
      // 数据代理 this.$data.title ==> this.title
      proxy(data) {
        Object.keys(data).forEach(key => {
          Object.defineProperty(this, key, {
            get() {
              return this.$data[key]
            },
            set(v) {
              this.$data[key] = v
            }
          })
        })
      }
    
    
      // 响应式数据
      // data数据中如果只为null,typeof null 只为object
      observe(data) {
        if (typeof data !== 'object' || data === null) {
          return;
        }
        Object.keys(data).forEach(key => {
          // 若this.form 的值: { a: 1 }, 递归处理所有乘次
          this.observe(data[key])
    
          new Observe(this, data, key)
    
        })
      }
    
    
      // 挂载:1. 实例化Watcher; =>  2. 将updateComponent组件更新方法传入watcher中,用于①初始化 ②组件更新; =>  3. 将updateComponent组件更新方法中,reader获取vnode树, 用_update(vnode)方法转为真实dom
      $mount(el) {
        this.$el = document.querySelector(el)
        // reader函数是用来获取vnode树的,形参h: $createElement => 将输入的参数变成vnode
        const updateComponent = () => {
          const {reader} = this.$options
          const vnode = reader.call(this, this.$createElement)
          console.log(vnode);
          // 将vnode转为真实dom
          this._update(vnode)
        }
    
        new Watcher(this, updateComponent)
      }
    
      // vnode -> dom
      // 获取上一次更新的vnode树: ① 如果不存在,则为初始化; ② 如果存在,则为更新操作
      // __patch__方法: 将虚拟节点=>dom,并更新到视图上
      _update(vnode) {
        // 获取上一次的vnode树
        const prevVnode = this._vnode // this._vnode 在__patch__方法(即diff算法时,进行存储的)
        if (!prevVnode) {
          this.__patch__(this.$el, vnode)
        } else {
          this.__patch__(prevVnode, vnode)
        }
      }
    
      // __patch__: 补丁函数: 将需要更新的节点,从vnode => dom ,打补丁到页面对应的dom上(定点更新)
      // 两个参数:① oldVnode:老节点; ② newVnode:新节点 => vnode:{tag, props, children}
      // ①判断oldVnode是否是真实dom,如果是真实dom,则为初始化,直接将newVnode => dom, 追加到oldVnode这个真实dom上去
      // ②oldVnode是虚拟dom,则比较oldVnode、newVnode之间不同,进行diff算法增加、删除
      // ②-1: 判断oldVnode、newVnode是否是同一个节点(通过比较tag、key,如果一样,则同一个节点)
      // ②-2: 若是同一个节点,则进行propsOps属性更新、childrenOps子节点更新
      // ②-3: 不是同一个节点,则进行节点替换
      __patch__(oldVnode, newVnode) {
        if (oldVnode.nodeType) {
          const parent = oldVnode.parentNode
          const nextNode = oldVnode.nextSibling
          // vnode => dom
          const el = this.createElm(newVnode)
          parent.insertBefore(el, nextNode)
          parent.removeChild(oldVnode)
          this._vnode = newVnode
    
        } else { // 更新
          // 判断是否为同一个元素:tag相同,key相同,这里就不考虑key了
          if (oldVnode.tag === newVnode.tag) {
            // 获取oldVnode对应的真实dom, 用于做真实的dom操作, 并将这个真实dom,存储到新节点的el变量上,以方便下次更新是使用
            const el = newVnode.el = oldVnode.el // const el = oldVnode.el;newVnode.el = el
            // 属性更新
            this.propsOps(el, oldVnode, newVnode)
    
            // children更新
            this.childrenOps(el, oldVnode, newVnode)
    
          }
          // 不是同一个元素
          else {
            // todo
            // el.parentNode.replaceChild(this.createElm(newVnode), el)
            const oldEl = oldVnode.el
            const parentEl = oldEl.parentNode
            const newEl = this.createElm(newVnode)
            newVnode.el = newEl
            parentEl.replaceChild(newEl, oldEl)
          }
          this._vnode = newVnode
        }
      }
    
      // children更新
      // 获取新旧节点的children:oldCh、newCh
      // 判断新节点的children值,是否是字符串 => newCh是字符串 => 判断① oldCh是字符串,oldCh与newCh如果不相同,则替换,设置textContent值; =>  ② oldCh是数组,有节点,则el直接替换,设置textContent值;
      // newCh是数组 => 如果① oldCh是字符串 => 则遍历新节点的newCh,对每一个子节点vnode,进行vnode->dom,即createElm方法,再追加到el上  => ② oldCh是数组,有子节点 => 用diff算法,更新子节点
      childrenOps(el, oldVnode, newVnode) {
        const oldCh = oldVnode.children
        const newCh = newVnode.children
    
        if (typeof newCh === 'string') {
          if (typeof oldCh === 'string') {
            if (newCh !== oldCh) {
              el.textContent = newCh
            }
          } else {
            el.textContent = newCh
          }
        }
        // 新节点的children是数组,即:新节点有子节点
        else {
          if (typeof oldCh === 'string') {
            el.textContent = ''
            newCh.forEach(childVnode => {
              const childElm = this.createElm(childVnode)
              el.appendChild(childElm)
            })
          }
          // 新旧节点都有子节点:更新子节点
          else {
            this.updateChildren(el, oldCh, newCh)
          }
        }
    
      }
    
      // 更新子节点
      // 前提:新旧节点都有子节点
      // ① 在新旧vnode树下,取最小vnode树 => 取两个数组最小长度minLen
      // ② 遍历最小长度minLen,比较新旧vnode树下对应的子节点,是否相同(tag、key是否相同),相同节点则更新属性、children; 不相同,则删除旧节点,新增节点 => 就是this.__patch__(oldVnode, newVnode)==> 打补丁,直接把节点新增上去
      // ③ 判断新旧vnode树,谁长?
      // ④ 旧节点树长 => 则把旧vnode树截取(minLen => length)这么长,再遍历,删除el中的旧节点
      // ⑤ 新节点树长 => 则把新vnode树截取(minLen => length)这么长,再遍历,新增el中的新节点
    
      updateChildren(parentEl, oldCh, newCh) {
        const minLen = Math.min(oldCh.length, newCh.length)
        for (let i = 0; i < minLen; i++) {
          this.__patch__(oldCh[i], newCh[i])
        }
        if (oldCh.length > newCh.length) {
          oldCh.slice(minLen).forEach(child => {
            parentEl.removeChild(this.createElm(child))
          })
        }
        if (oldCh.length < newCh.length) {
          newCh.slice(minLen).forEach(child => {
            parentEl.appendChild(this.createElm(child))
          })
        }
      }
    
      // 节点属性操作
      // 分别获取新旧节点的属性列表
      // 遍历新节点属性列表 => 判断新节点属性在旧节点中是否存在 => 若不存在,则dom直接新增该属性; => 存在,看新旧节点属性值是否相同,相同就不用处理,不相同,就重新设置属性值
      propsOps(el, oldVnode, newVnode) {
        const oldProps = oldVnode.props || {}
        const newProps = newVnode.props || {}
    
        for (let propName in newProps) {
          if (!propName in oldProps) {
            el.removeAttribute(propName)
          } else if (oldProps[propName] !== newProps[propName]) {
            el.setAttribute(propName, newProps[propName])
          }
        }
      }
    
      // vnode => dom
      // vnode:{tag, props, children}
      createElm(vnode) {
        const el = document.createElement(vnode.tag)
    
        // 有props
        if (vnode.props) {
          Object.keys(vnode.props).forEach(prop => {
            el.setAttribute(prop, vnode.props[prop])
          })
        }
    
        // 有children
        if (vnode.children) {
          if (typeof vnode.children === 'string') {
            el.textContent = vnode.children
          } else {
            vnode.children.forEach(child => {
              const childNode = this.createElm(child)
              el.appendChild(childNode)
            })
          }
        }
    
        vnode.el = el
        return el
      }
    
      $createElement(tag, props, children) {
        return {tag, props, children}
      }
    
    }
    

    定义数据响应式 observer代码

    / 定义数据响应式
    // 1. data中每一个属性,都定义一个Dep,用来收集依赖
    // 2. 读取key的时候,收集一个依赖
    // 3. 数据发生改变的时候,通知更新
    class Observe {
      constructor(vm, data, key) {
        this.$vm = vm
        this.defineReactive(data, key, data[key])
      }
    
      defineReactive(data, key, val) {
        const vm = this.$vm
        const dep = new Dep()
        Object.defineProperty(data, key, {
          get() {
            if (Dep.target) {
              dep.addDep(Dep.target)
            }
            return val
          },
          set(v) {
            if (v !== val) {
              // 考虑到用户将this.title以前为string字符串,后进行重新赋值为this.title = {name: '我是标题'}
              // 重新对新值v做响应式处理
              vm.observe(v)
    
              val = v
              // 通知依赖更新
              dep.notice()
            }
          }
        })
      }
    
    }
    

    依赖收集 Dep代码

    class Dep {
      constructor() {
        this.deps = new Set()
      }
    
      addDep(dep) {
        this.deps.add(dep)
      }
    
      notice() {
        this.deps.forEach(dep => {
          dep.update()
        })
      }
    }
    

    watcher代码

    // 一个组件只有一个watcher
    // 参数callback: 组件发生改变时,触发的组件更新函数updateComponent
    class Watcher {
      constructor(vm, callback) {
        this.$vm = vm
        this.$cb = callback
        // vue实例化时,执行更新函数:初始化
        this.getter()
      }
    
      // 触发 收集依赖、执行渲染函数
      getter() {
        // wathcer实例赋值给Dep.target,方便data中数据get()方法中进行收集
        Dep.target = this
    
        // 执行渲染函数:
        this.$cb.call(this.$vm)
        console.log('vue实例化,updateComponent组件更新方法执行!')
    
        Dep.target = null
      }
    
      update() {
        // 组件更新时,执行组件渲染函数
        this.getter()
      }
    }
    
  • 相关阅读:
    Android零基础入门第34节:Android中基于监听的事件处理
    【洛谷】3953:逛公园【反向最短路】【记忆化搜索(DP)统计方案】
    【洛谷】1608:路径统计 1144:最短路计数
    【洛谷】1081:跑路【倍增】【最短路】
    照着例子学习protobuf-python
    NodeJs与ActionScript的GET和POST通讯
    编程语言应用领域(转)
    我是C#上层转到嵌入式和单片机的
    转发一位老师的文章,希望能给你带来帮助
    沈逸的IT专栏博客记录
  • 原文地址:https://www.cnblogs.com/shine-lovely/p/14809249.html
Copyright © 2020-2023  润新知