重学vue(2, 3)及其生态+TypeScript 之 vue部分实现和源码分析(未完待续)

简介: 重学vue(2, 3)及其生态+TypeScript 之 vue部分实现和源码分析(未完待续)

HTML如何渲染到浏览器


我们传统的前端开发中,我们是编写自己的HTML,最终被渲染到浏览器上的,那么它是什么样的过程呢?


直接通过编写的html元素,渲染成真实的dom树,然后就渲染到浏览器了。


虚拟DOM


但是前端框架现在都采用的是虚拟dom来构建页面。那虚拟dom有什么优势呢?


  • 首先是可以对真实的元素节点进行抽象,抽象成VNode(虚拟节点),这样方便后续对其进行各种操作。因为对于直接操作DOM来说是有很多的限制的,比如diff、clone等等,但是使用JavaScript编程语言来操作这些就变得非常的简单。


  • 我们可以使用JavaScript来表达非常多的逻辑,而对于DOM本身来说是非常不方便的。


  • 其次是方便实现跨平台,包括你可以将VNode节点渲染成任意你想要的节点。如渲染在canvas、WebGL、SSR、Native(iOS、Android)上。


  • 并且Vue允许你开发属于自己的渲染器(renderer),在其他的平台上渲染。 虚拟DOM的渲染过程

网络异常,图片无法展示
|


网络异常,图片无法展示
|


vue中三大核心系统


事实上Vue的源码包含三大核心:


  • Compiler模块:编译模板系统。将template模板中的内容渲染成VDOM。


  • Runtime模块:也可以称之为Renderer模块,真正渲染的模块。将VDOMxua渲染成真实的DOM。


  • Reactivity模块:响应式系统。


网络异常,图片无法展示
|


三大系统协同工作


网络异常,图片无法展示
|


手动实现一些功能


了解了vue的构建过程,那么就来实现一些vue模块功能吧。


渲染系统模块


  • 功能一:h函数,用于返回一个VNode对象。 实现非常简单。我们知道h函数它接收三个参数,然后返回一个VNode对象。


const h = (tag, props, children) => {
      return {
        tag,
        props,
        children
      }
    }


  • 功能二:mount函数,用于将VNode挂载到DOM上。 这个函数的实现也很简单


  • 先使用传入的vNode的tag创建一个父节点。


  • 然后判断props属性,循环添加到父节点上。(这里我们就只判断了传入事件和其他属性的情况)


  • 再然后就是判断vNode中的children。(我们只判断了字符串和数组类型)。如果是数组类型,我们就递归调用mount函数即可,将子vNode添加到父节点上。


  • 最后将父节点挂载到传入的根节点上。


/**
     * 将虚拟节点挂载到真实的dom上
     */
    function mount (vNode, container) {
      // 创建一个父节点
      const el = vNode.el = document.createElement(vNode.tag);
      // 将vNode中的属性绑定到真实的dom上
      if (Object.keys(vNode.props).length) {
        for (let key in vNode.props) {
          if (key.startsWith("on")) { // 该属性值为一个事件时
            el.addEventListener(key.slice(2).toLowerCase(), vNode.props[key])
          } else { // 该属性值为其他值时
            el.setAttribute(key, vNode.props[key])
          }
        }
      }
      // 处理子节点children。这里我们只处理children为字符串和数组的情况
      if (vNode.children) {
        if (typeof vNode.children === 'string') { //当children传入的是一个字符串时,直接插入到当前父节点中
          el.innerHTML = vNode.children
        } else {//当children传入的是一个数组时,需要递归调用mount,来做处理
          for (let i in vNode.children) {
            mount(vNode.children[i], el)
          }
        }
      }
      // 将创建的节点挂载到container上。
      container.appendChild(el)
    }


通过上面两个方法,我们就可以将vNode转化成真实的dom了。下面来看一下例子。


<div id="id"></div>
    <script src="./renderer.js"></script>
    const vNode = h(
      "div",
      { class: "name", onClick: () => { console.log("绑定事件") } },
      [h("p", {
        class: 'p'
      }, "我的p标签")])
    mount(vNode, document.getElementById("id"))


网络异常,图片无法展示
|


  • 功能三:patch函数,用于对两个VNode进行对比,决定如何处理新的VNode。我们的实现都是基于js提供的API来处理dom


patch函数的实现,分为两种情况


  • n1和n2是不同类型的节点:(这种情况处理起来非常简单)


  • 找到n1的el父节点,删除原来的n1节点的el。


  • 挂载n2节点到n1的el父节点上。


  • n1和n2节点是相同的节点:


  • 处理props的情况


  • 先将新节点的props全部挂载到el上。


  • 判断旧节点的props是否不需要在新节点上(这个是判断旧节点中的属性是否在新节点中),如果不需要,那么删除对应的属性。


  • 处理children的情况


  • 如果新节点是一个字符串类型,那么直接调用 el.innerHTML = newChildren。


  • 如果新节点不是一个字符串类型。


  • 旧节点是一个字符串类型


  • 将el的innerHTML设置为空字符串。


  • 遍历新节点,调用mount方法,将节点挂载到当前el上。


  • 旧节点也是一个数组类型


  • 取出数组的最小长度。循环调用patch方法,对比新旧节点。


  • 当oldChildren多余newChildren,那么将删除多余的旧vNode。其余的递归调用patch即可


  • 当oldChildren少余newChildren,那么将添加多余的新vNode。其余的递归调用patch即可


/**
     * 
     * @param {vNode} n1 旧vNode
     * @param {vNode} n2 新vNode
     */
    function patch (n1, n2) {
      // 将旧vNode的父节点赋值给新vNode.el。
      const el = n2.el = n1.el;
      if (n1.tag !== n2.tag) { // 如果两个vNode中的tag不同。直接删除旧vNode。添加新vNode。
        n1.el.parentElement.removeChild(n1.el)
        mount(n2, n1.el.parentElement)
      } else { // vNode的tag不同
        // 处理props。
        for (let key in n2.props) {
          const oldProp = n1.props[key];
          const newProp = n2.props[key]
          if (oldProp !== newProp) { //如果旧vNode中不存在新vNode属性,将新属性添加即可
            if (key.startsWith("on")) { //处理事件
              el.addEventListener(key.slice(2).toLowerCase(), newProp)
            } else { // 处理其他值
              el.setAttribute(key, newProp)
            }
          }
        }
        // 删除旧vNode中的属性
        for (let key in n1.props) {
          if (key.startsWith("on")) { // 对事件监听的判断,移除所有事件
            const oldProp = n1.props[key];
            el.removeEventListener(key.slice(2).toLowerCase(), oldProp)
          }
          if (!(key in n2.props)) { // 移除所有新vNode中没有的属性
            el.removeAttribute(key);
          }
        }
        const oldChildren = n1.children || []
        const newChildren = n2.children || []
        // 处理children
        if (typeof newChildren === 'string') { // 处理字符串情况
          if (typeof oldChildren === 'string') {
            if (!(oldChildren === newChildren)) { // 都是字符串,并且不相等
              el.innerHTML = newChildren
            }
          } else { // oldChildren不是字符串,也将被覆盖
            el.innerHTML = newChildren
          }
        } else { // 处理数组情况
          if (typeof oldChildren === 'string') { // oldChildren是字符串类型
            // 清除el中的内容
            el.innerHTML = ''
            for (let vNode in newChildren) {
              mount(vNode, el)
            }
          } else { // oldChildren是数组类型
            // oldChildren: [v1, v2, v3, v8, v9]
            // newChildren: [v1, v5, v6]
            // 我们只处理对应位置的vNode。简单处理
            // 当oldChildren多余newChildren,那么将删除多余的旧vNode。其余的递归调用patch即可
            // 当oldChildren少余newChildren,那么将添加多余的新vNode。其余的递归调用patch即可
            const minChildrenLength = Math.min(oldChildren.length, newChildren.length)
            for (let i in minChildrenLength) {
              patch(oldChildren[i], newChildren[i])
            }
            if (oldChildren.length > minChildrenLength) {
              // 删除多余的oldChildren
              oldChildren.slice(minChildrenLength).forEach(vNode => {
                el.removeChild(vNode.el)
              })
            }
            if (newChildren.length > minChildrenLength) {
              // 删除多余的oldChildren
              newChildren.slice(minChildrenLength).forEach(vNode => {
                el.appendChild(vNode.el)
              })
            }
          }
        }
      }
    }


现在我们就可以做到vNode -> 真实dom -> 监听新旧vNode的变化做出改变。下面来通过一个例子测试以上代码。


<div id="id"></div>
  <script src="./renderer.js"></script>
  <script>
    const vNode = h(
      "div",
      { class: "name", onClick: () => { console.log("绑定事件") } },
      [h("p", {
        class: 'p'
      }, "我的p标签")])
    mount(vNode, document.getElementById("id"))
    const vNode1 = h(
      "div",
      { class: "llmzh", onClick: () => { console.log("我的事件") } },
      "直接字符串")
    setTimeout(() => {
      patch(vNode, vNode1)
    }, 1000)
  </script>


网络异常,图片无法展示
|


响应式系统


实现响应式系统的主要步骤就是对象劫持。


vue2中实现响应式系统。我们通过definePropertyAPI来实现对象劫持。


class Dep {
      constructor() {
        this.subscribers = new Set();
      }
      depend() {
        if (activeEffect) {
          this.subscribers.add(activeEffect);
        }
      }
      notify() {
        this.subscribers.forEach(effect => {
          effect();
        })
      }
    }
    let activeEffect = null;
    function watchEffect(effect) {
      activeEffect = effect;
      effect();
      activeEffect = null;
    }
    // Map({key: value}): key是一个字符串
    // WeakMap({key(对象): value}): key是一个对象, 弱引用
    const targetMap = new WeakMap();
    function getDep(target, key) {
      // 1.根据对象(target)取出对应的Map对象
      let depsMap = targetMap.get(target);
      if (!depsMap) {
        depsMap = new Map();
        targetMap.set(target, depsMap);
      }
      // 2.取出具体的dep对象
      let dep = depsMap.get(key);
      if (!dep) {
        dep = new Dep();
        depsMap.set(key, dep);
      }
      return dep;
    }
    // vue2对raw进行数据劫持
    function reactive(raw) {
      Object.keys(raw).forEach(key => {
        const dep = getDep(raw, key);
        let value = raw[key];
        Object.defineProperty(raw, key, {
          get() {
            // 将依赖函数传入到set中
            dep.depend();
            return value;
          },
          set(newValue) {
            if (value !== newValue) {
              value = newValue;
              // 调用该依赖相关的所有函数
              dep.notify();
            }
          }
        })
      })
      return raw;
    }


vue3中实现响应式系统。我们通过ProxyAPI来实现对象劫持。


// vue3对raw进行数据劫持
    function reactive(raw) {
      return new Proxy(raw, {
        get(target, key) {
          const dep = getDep(target, key);
          dep.depend();
          return target[key];
        },
        set(target, key, newValue) {
          const dep = getDep(target, key);
          target[key] = newValue;
          dep.notify();
        }
      })
    }


为什么Vue3选择Proxy呢?


  • 如果新增元素, Object.definedProperty 劫持对象的属性时。那么Vue2需要再次 调用definedProperty,而 Proxy 劫持的是整个对象,不需要做特殊处理。


  • 修改对象的不同。使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截,而使用 proxy 就必须修改代理对象,即 Proxy 的实例才可以触发拦截。


  • Proxy 能观察的类型比 defineProperty 更丰富。例如has:in操作符的捕获器。deleteProperty:delete 操作符的捕捉器,等等其他操作。


相关文章
|
3月前
|
JavaScript 前端开发 安全
【技术革新】Vue.js + TypeScript:如何让前端开发既高效又安心?
【8月更文挑战第30天】在使用Vue.js构建前端应用时,结合TypeScript能显著提升代码质量和开发效率。TypeScript作为JavaScript的超集,通过添加静态类型检查帮助早期发现错误,减少运行时问题。本文通过具体案例展示如何在Vue.js项目中集成TypeScript,并利用其类型系统提升代码质量。首先,使用Vue CLI创建支持TypeScript的新项目,然后构建一个简单的待办事项应用,通过定义接口描述数据结构并在组件中使用类型注解,确保代码符合预期并提供更好的编辑器支持。
84 0
|
3月前
|
JavaScript 前端开发 安全
立等可取的 Vue + Typescript 函数式组件实战
立等可取的 Vue + Typescript 函数式组件实战
|
4月前
|
JavaScript 前端开发
【Vue3+TypeScript】CRM系统项目搭建之 — 关于如何设计出优质的 Vue 业务组件
【Vue3+TypeScript】CRM系统项目搭建之 — 关于如何设计出优质的 Vue 业务组件
49 0
【Vue3+TypeScript】CRM系统项目搭建之 — 关于如何设计出优质的 Vue 业务组件
|
5月前
|
JavaScript 安全 前端开发
Vue 3 中的 TypeScript
【6月更文挑战第15天】
83 6
|
6月前
|
JavaScript 前端开发 开发者
类型检查:结合TypeScript和Vue进行开发
【4月更文挑战第24天】TypeScript是JavaScript超集,提供类型注解等特性,提升代码质量和可维护性。Vue.js是一款高效前端框架,两者结合优化开发体验。本文指导如何配置和使用TypeScript与Vue:安装TypeScript和Vue CLI,创建Vue项目时选择TypeScript支持,配置`tsconfig.json`,编写`.tsx`组件,最后运行和构建项目。这种结合有助于错误检查和提升开发效率。
56 2
|
6月前
|
JavaScript 前端开发 开发者
Vue工具和生态系统: Vue.js和TypeScript可以一起使用吗?
【4月更文挑战第18天】Vue.js与TypeScript兼容,官方文档支持在Vue项目中集成TypeScript。TypeScript作为JavaScript超集,提供静态类型检查和面向对象编程,增强代码准确性和健壮性。使用TypeScript能提前发现潜在错误,提升代码可读性,支持接口和泛型,使数据结构和函数更灵活。然而,不是所有Vue插件都兼容TypeScript,可能需额外配置。推荐尝试在Vue项目中使用TypeScript以提升项目质量。
113 0
|
6月前
|
JavaScript 前端开发
在Vue中使用TypeScript的常见问题有哪些?
在Vue中使用TypeScript的常见问题有哪些?
98 2
|
8天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
8天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。