重学Vue

作者: 人猿Jim | 来源:发表于2021-12-02 17:23 被阅读0次
SWKL

写了两年的vue一直没有对它进行过总结,现在闲下来可以填填坑了。

VDOM

什么是虚拟DOM?虚拟DOM的作用。
老生常谈的问题了。如果说真实的DOM是一个大厦,那么说虚拟DOM就是这个大厦的设计图。虚拟DOM的更新效率并不一定比直接命令式的更新真实DOM要快,没有任何框架可以比纯手动的优化 DOM 操作更快,它的最主要目的是:

  1. 把真实DOM抽象出来,通过新旧对比获取最小代价更新真实DOM的方法。
  2. 更高的可维护性,相对于JQ命令式更新DOM,Vue、React框架中运用虚拟DOM可以更好地维护应用。
  3. 可以渲染到 DOM 以外的平台,实现 SSR、同构渲染这些高级特性Weex 等框架应用的就是这一跨端特性。

网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么?
Vue采用虚拟DOM的目的是什么?

Vue的组成与工作原理

Vue 的三个核心模块:

Reactivity Module 响应式模块
Compiler Module 编译器模块
Renderer Module 渲染模块

响应式模块允许我们创建 JavaScript 响应对象并可以观察其变化。当使用这些对象的代码运行时,它们会被跟踪,因此,它们可以在响应对象发生变化后运行。

编译器模块获取 HTML 模板并将它们编译成渲染函数(render function)。这可能在运行时在浏览器中发生,但在构建 Vue 项目时更常见。这样浏览器就可以只接收渲染函数。

渲染模块的代码包含在网页上渲染组件的三个不同阶段:

渲染阶段
挂载阶段
补丁阶段
在渲染阶段,将调用 render 函数,它返回一个虚拟 DOM 节点。
在挂载阶段,使用虚拟DOM节点并调用 DOM API 来创建网页。
在补丁阶段,渲染器将旧的虚拟节点和新的虚拟节点进行比较并只更新网页变化的部分。

现在让我们来看一个例子,一个简单组件的执行。它有一个模板,以及在模板内部使用的响应对象。首先,模板编译器将 HTML 转换为一个渲染函数。然后初始化响应对象,使用响应式模块。接下来,在渲染模块中,我们进入渲染阶段。这将调用 render 函数,它引用了响应对象。我们现在监听这个响应对象的变化,render 函数返回一个虚拟 DOM 节点。接下来,在挂载阶段,调用 mount 函数使用虚拟 DOM 节点创建 web 页面。最后,如果我们的响应对象发生任何变化,正在被监视,渲染器再次调用render函数,创建一个新的虚拟DOM节点。新的和旧的虚拟DOM节点,发送到补丁函数中,然后根据需要更新我们的网页。

参考
跟尤雨溪一起解读Vue3源码【中英字幕】- Vue Mastery
Vue 3 Deep Dive with Evan You

手写Mini Vue

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<style>
  .red {
    color: red;
  }
</style>

<body>
  <div id="app"></div>
</body>
<script src="./reactivity.js"></script>
<script>
  // 第一部分compiler 编译成 render function => 形成虚拟dom 
  // 由于模板编译过于复杂,这里直接使用其编译结果,即vdom
  function h(tag, props, children) {
    return {
      tag,
      props,
      children
    }
  }
  // 第二部分 挂载到真实dom中 ,即mount方法
  function mount(vdom, root) {
    const el = vdom.el = document.createElement(vdom.tag)
    // props
    const props = vdom.props
    if (props) {
      for (const key in props) {
        if (Object.hasOwnProperty.call(props, key)) {
          if (key.startsWith('on')) {
            document.addEventListener(key.slice(2).toLowerCase(), props[key])
          }
          el.setAttribute(key, props[key])
        }
      }
    }

    // children
    if (vdom.children) {
      const child = vdom.children
      if (typeof child === 'string' || typeof child === 'number') {
        el.textContent = child
      } else {
        child.forEach(currentChild => {
          mount(currentChild, el)
        });
      }
    }
    root.appendChild(el)
  }
  // 第三部分 实现响应式 在reactivity文件中

  // 第四部分 实现patch方法 
  // 思路:diff Vdom = 检查两个vdom中有何不同 = 比较各个方面 => 有何方面 => 检查Vdom的结构 
  // Vdom结构: { tag, props, children } => 从 tag , props , children 检查
  // 所以 diff 实际就是比较 tag,props,children
  function patch(oldNode, newNode) {
    const el = newNode.el = oldNode.el
    // 检查tag
    if (newNode.tag === oldNode.tag) {
      // 比较props
      const newProps = newNode.props
      const oldProps = oldNode.props
      // 1. 有新的属性添加
      for (const key in newProps) {
        const newPropsValue = newProps[key]
        const oldPropsValue = oldProps[key]
        if (Object.hasOwnProperty.call(newProps, key)) {
          if (oldPropsValue !== newPropsValue) {
            el.setAttribute(key, newPropsValue)
          }
        }
      }
      // 2.有旧的属性删除
      for (const key in oldProps) {
        if (Object.hasOwnProperty.call(oldProps, key)) {
          if (!key in newProps) {
            el.removeAttribute(key)
          }
        }
      }
      // 比较children
      // 目前children有两种形式 string字符串 和 数组
      const oldChild = oldNode.children
      const newChild = newNode.children
      // 1.如果新children是字符串
      if (typeof newChild === 'string') {
        if (typeof oldChild === 'string' && oldChild !== newChild) {
          el.textContent = newChild
        } else {
          el.textContent = newChild
        }
      } else {
        // 2.如果新children是数组
        if (typeof oldChild === 'string') {
          el.innerHTML = ''
          newChild.forEach(child => {
            mount(child, el)
          });
        } else {
          // 2.1 如果两者都是数组如何比较
          // Vue有两种比较方法,一种是提供key作为提示(稍稍复杂),一种是粗暴的比较
          // 这里用粗暴的比较:比较同一index下的子项类型是否一致
          const commonLength = Math.min(oldChild.length, newChild.length)
          for (let i = 0; i < commonLength; i++) {
            patch(oldChild[i], newChild[i])
          }
          if (newChild.length > commonLength) {
            newChild.slice(commonLength).forEach(child => mount(child, el))
          } else if (newChild.length < oldChild.length) {
            oldChild.slice(commonLength).forEach(child => el.removeChild(child.el))
          }
        }
      }

    } else {
      // replace
    }
  }

  // 第五部分 将以上的连接起来 
  const App = {
    data: reactive({
      count: 0
    }),
    methods: {

    },
    render() {
      return h('div', { class: 'red', onClick: () => this.data.count++ }, String(this.data.count))
    }
  }
  function createApp(app, root) {
    let isMounted = false
    let prevDom
    watchEffect(() => {
      if (!isMounted) {
        prevDom = app.render()
        mount(prevDom, root)
        isMounted = true
      } else {
        const newDom = app.render()
        patch(prevDom, newDom)
        prevDom = newDom
      }
    })

  }
  createApp(App, document.querySelector('#app'))

</script>

</html>

响应式系统

// reactivity.js
let activeEffect
function watchEffect(effect) {
  activeEffect = effect
  effect()
  activeEffect = null
}
class Dep {
  subscriber = new Set()
  track() {
    activeEffect && this.subscriber.add(activeEffect)
  }
  trigger() {
    this.subscriber.forEach(effect => effect())
  }
}

const DepMap = new WeakMap()
function getDep(target, key) {
  let targetMap = DepMap.get(target)
  if (!targetMap) {
    DepMap.set(target, targetMap = new Map())
  }
  let dep = targetMap.get(key)
  if (!dep) {
    targetMap.set(key, dep = new Dep())
  }
  return dep
}
const reactiveHandler = {
  get(target, key, receiver) {
    const dep = getDep(target, key)
    dep.track()
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    const dep = getDep(target, key)
    const res = Reflect.set(target, key, value, receiver)
    dep.trigger()
    return res
  }
}
function reactive(obj) {
  return new Proxy(obj, reactiveHandler)
}

相关文章

  • 重学Vue

    写了两年的vue一直没有对它进行过总结,现在闲下来可以填填坑了。 VDOM 什么是虚拟DOM?虚拟DOM的作用。老...

  • 重学vue(基本运行)

    谈不上重学,因为第一次也没有学好,希望这次可以一次成功。 之前有几个条件 安装node,查看node版本 安装淘宝...

  • 重学Vue--Vue生命周期

    什么是Vue的生命周期? 从Vue实例创建、运行、到销毁期间,总是伴随着各种各样的事件,这些事件,统称为生命周期。...

  • 重学Vue(一)—— 前端路由

    Vue写了段时间,开发页面起来已经是唰唰唰了,但是对其中原理却知之甚少。那可不行,为了不错过任何的学习点(在面试官...

  • vue.js入门(一,第一个程序,非新手绕)

    为什么要重学Vue,或者说Vue的好处是什么,用一句话概括就是:数据驱动模板。在实际项目中,js和dom的交互会非...

  • 杂谈!了解一些额外知识,让你的前端开发锦上添花

    在前端学习里面,很多人都是注重学习代码(html,css,js)。或者是一些框架,库(jquery,vue,rea...

  • 学了 Vue 还需要学 React 吗?

    最近在重学 React,很多小伙伴发出 “学完 Vue 还需要学 React?” 这样的疑问,下面我们就来探讨一下...

  • props遇到的一些问题

    今天重学vue基础时,突发奇想在props上使用了自己瞎定义的一个单词,刚好就有一个大写字母:itemAtr==>...

  • 重学Vue--keep-alive的使用

    keep-alive是什么? keep-alive 是 Vue 的内置组件,当它包裹动态组件时,会缓存不活动的组件...

  • 理解模板语法 | 重学Vue3

    前言 这个月开始重新开始学习Vue3,从理解基本使用到模拟实现! 本文内容是关于Vue的模板语法 基本目录如下: ...

网友评论

    本文标题:重学Vue

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