最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 看得懂的 Vue 框架核心原理(上)

    正文概述 掘金(SamChord)   2020-11-29   549

    看得懂的 Vue 框架核心原理(上) Vue 作为当下流行的三大前端框架之一,简单易用的特性深受大家的喜爱,也一直是大家平时做业务开发项目的不二利器。但是,光会用肯定不行,不明白框架内部的实现和设计思想,在使用过程中碰到问题难免会有点懵。自古有云,知其然,亦知其所以然。接下来从简单的例子一步步深入了解 Vue 框架的核心原理。

    本主题为笔者平时学习整理所得,内容分为两个篇章,此为上篇。

    1.三大核心板块

    众所周知,Vue 框架内部由三大核心板块组成,分别是响应式板块编译板块渲染板块看得懂的 Vue 框架核心原理(上)

    • 响应式板块:使用 JavaScript 根据组件实例构建响应式数据对象
    • 编译板块: 把 template 的内容传递给 render 函数,并返回 vnode(即虚拟 dom)
    • 渲染板块:接受 vnode 并通过比较渲染成真实的 dom

    由于 Vue2.0Vue3.0 针对构建响应式数据对象使用了不同的方式,2.0 是 Object.defineProperty,3.0 是 proxy。内容较多,故把响应式板块放到下一个篇章讲解,此篇章先讲解后两个板块。

    2.编译和渲染板块的简单实现

    2.1 框架出现的背景及意义

    话不多说,直奔主题。文章标题写的是“看得懂”,故这里拿一个最基本的例子做实现,相信能够很好地帮助大家理解框架原理。

    假设我们就实现一个最简单的内容,在页面上显示一个 Hello World。显示效果如下: 看得懂的 Vue 框架核心原理(上)
    如果让我们在没有框架的时代背景下完成这个效果。一般是创建一个标签,然后修改它的 textContent,并赋上颜色:

    let iDiv = document.createElement('div')
    iDiv.textContent = "Hello World";
    iDiv.style.color = "red";
    document.body.appendChild(iDiv)
    

    这样写没什么问题,而且在 jQuery 时代配合用上相关 api,前端行业也是一片欣欣向荣的景象。然而,时间久了,些许问题就暴露出来了。主要归纳为下面几项:

    • 频繁操作 DOM 元素:页面是由元素标签组成的,要想更新元素内容,需要开发者频繁地操作计算元素内容;
    • 数据更新效率低:变更的数据有大有小,针对大批量数据更新或者多层级 DOM 结构少量数据变更没有较好的更新策略;
    • 缺少模块化:这里指的是 HTML 的模块化,虽然 W3C 有推出 Web Components标准想要从自身就支持 HTML 的模块化,但就目前来看还不是很完善,而且也不是所有主流浏览器都支持。

    针对以上三个问题,我们可以思考分析,并尝试找出解决方案。首先,第一个和第三个问题可以归为一类,都是关于 HTML 的,我们知道原生元素标签上的属性值很多,光一个 div 标签就包含几十上百个属性值:

    看得懂的 Vue 框架核心原理(上)

    这些内容是在刚才调用 document.createElement 就会生成的,而我们实际操作的时候只用到了 textContentstyle 两个属性,那么我们是不是可以构建一个只包含我们关注内容的对象呢。答案是可以,着就催生出了一个概念 Virtual dom,即用 js 对象去模拟真实的 DOM 结构,并且 js 还支持模块化。其次,针对第二个问题,在虚拟 DOM 的基础上,落地了各种各样的 DOM diff算法,提升更新效率。

    说了这么多总结一下:框架用虚拟 DOM 模仿真实 DOM,其内部既把 DOM 实现了模块化,又能渲染数据并进行高效率的更新。开发者只需要调用 api 把相关数据进行传递赋值即可。

    3.代码实操

    基于上面的内容,以下代码会包含 Vue 框架三个主要函数的实现:

    • render 函数,别名 h;用于构建 vnode
    • mount 函数,将 vnode 渲染为真实节点
    • patch 函数,用于更新操作

    3.1 基本结构

    还是刚才的例子,先假设基本结构如下:

    <style>
      .red {color: #f00;font-size:24px;}
    </style>
    <div id="app"></div>
    

    接下来是每个方法的具体实现。

    3.2 Render 方法

    现在要在 div 里渲染出 Hello World!。用过 Vue 的同学应该都清楚 h 这个方法的使用方式:

    const vdom = h('div',{class:'red'},[
      h('span',null,['hello'])
    ])
    

    此处的 h 方法接受三个参数,并返回 vnode。本着简单实现原则,我们尽可能简单的构建虚拟 dom 结构:

    /**
    * tag:元素标签
    * props:元素相关属性
    * children:元素子节点
    **/
    function h(tag,props,children){
        return {
            tag,
            props,
            children
        }
    }
    

    该方法的任务就是把模板元素转为 vnode,并返回。在 Vue 的单文件组件实例中你肯定使用过 template 语法,Vue 框架内部就是把 template 内容解析(分成 parse、optimize 与 generate 三个阶段)为字符串并传入该方法并返回 vnode 对象,当然会有很多额外工作要处理,感兴趣的同学可以阅读源码。这里只是把核心思想表达出来。

    3.3 mount 方法

    然后,我们将 vnode 渲染成真实 DOM:

    mount(vdom,document.getElementById('app'))
    

    可以看到 mount 方法需要两个参数,用于把 vnode 挂载到真实的 dom 节点上。

    function mount(vnode,container){
        // 把container一层层传递下去
        const el = vnode.el = document.createElement(vnode.tag);
        // props 处理
        if(vnode.props){
            for(const key in vnode.props){
                const value = vnode.props[key];
                // props 有很多类型,诸如指令,类名,方法等,这里假设都是 attribute
                el.setAttribute(key,value)
            }
        }
       // 处理 children
       if(vnode.children){
         // 假设子节点是字符串
         if(typeof vnode.children === 'string'){
             el.textContent = vnode.children;
         }else{ 
             vnode.children.forEach(child=>{
                 mount(child,el)
             })
         }
       } 
       container.appendChild(el)
    }
    

    逐步分析代码内容:

    • 每个 vnode 都带有 tag 属性,可以据此创建真实 dom 元素标签,并赋值给每个 vnode 一个新属性 el,存放其真实 dom 结构
    • 然后处理 vnode 的 props。基于简单实现原则,我们假设元素 props 只涉及 attribute,所以我们遍历 props 对象,并把所有数据设置到节点 el 的 attr 上;
    • 然后处理 vnode 的 children,这里分两种情况:当子节点是文本节点,直接将它设置给 el.textContent 即可;当子节点是数组时,则遍历所有子元素,把当前 el 作为 container 递归调用 mount 方法渲染所有子节点;
    • 最后将 el 节点挂载到最初的 container 中,这里即是 <div id="app"></div>

    整理代码,即可在页面得到想要的效果(以下代码直接复制粘贴后运行即可):

    <html>
    <style>
      .red {color: #f00;font-size:24px;}
    </style>
    <div id="app"></div>
    <script>
      const vdom = h('div',{class:'red'},[
        h('span',null,['hello'])
      ])
      mount(vdom,document.getElementById('app'))
      
      function h(tag,props,children){
        return {
            tag,
            props,
            children
        }
      }
      function mount(vnode,container){
        // 把container一层层传递下去
        const el = vnode.el = document.createElement(vnode.tag);
        // props 处理
        if(vnode.props){
            for(const key in vnode.props){
                const value = vnode.props[key];
                // props 有很多类型,这里假设都是 attribute
                el.setAttribute(key,value)
            }
        }
       // 处理 children
       if(vnode.children){
         // 假设子节点是字符串
         if(typeof vnode.children === 'string'){
             el.textContent = vnode.children;
         }else{ 
             vnode.children.forEach(child=>{
                 mount(child,el)
             })
         }
       } 
       container.appendChild(el)
      }
    </script>
    </html>
    

    3.4 patch 方法

    项目开发,页面数据不可能一成不变,更新操作则显得尤为重要。本着简单实现基本原则,假设我们现在要把页面文字改为 Hello Vue,并给它换个颜色,效果如下:
    看得懂的 Vue 框架核心原理(上)

    开发者使用 Vue 框架进行该项操作时,应该是变更元素标签的 class 名,并更新元素里的文本内容。在 Vue 框架内部,则会据此重新生成一棵虚拟 DOM 树,它可能是这样的:

    const vdom2 = h('div',{class:'green'},[
        h('span',null,'Hello Vue!')
    ])
    

    然后操作完成后,会触发内部的 patch 方法。因为生成了两棵虚拟 DOM 树,patch 方法必然要对它们进行比较,基于简单实现原则,本文只考虑相同类型节点比较(Vue 源码会通过 key 值以及是否为静态节点等信息进行比较,感兴趣的同学可以阅读源码):

    function patch(n1,n2){
      // 假设前后节点类型没有发生变化
      if(n1.tag === n2.tag){}
    }
    

    接下来,分步骤处理数据。

    • 先处理 props
    const el = n2.el = n1.el;
    // 1.处理 props
    const oldProps = n1.props ||{};
    const newProps = n2.props || {};
    // 遍历新属性所有数据
    for(const key in newProps){
        const oldValue = oldProps[key];
        const newValue = newProps[key];
        // 如果新旧属性值不同,把新属性设置给当前元素
       if(newValue !== oldValue){
         el.setAttribute(key,newValue)
       } 
    }
    // 遍历旧属性所有数据
    for(const key in oldProps){
        // 如果新结构中没有对应属性,则说明要移除对应的属性值
        if(!(key in newProps)){
            el.removeAttribute(key)
        }
    }
    

    代码注释已经写得比较清楚。补充说明的是:这段代码基于新旧树是相同节点,分别比较新属性和旧属性内容。针对新属性,对比查看是否存在和旧属性不同的值,如果有就变更赋值。针对旧属性,遍历查看是否存在新属性中没有的值,如果不在新属性中,移除即可。

    • 然后处理子节点,这块内容较多,我们先整理思路:

    1.新节点是字符串类型时,有两种情况要处理:旧节点是字符串,旧节点不是字符串;
    2.新节点不是字符串时,也有两种情况要处理:旧节点是字符串,旧节点不是字符串;

    先处理第一步:

    // 2.处理子节点
    const oldChildren = n1.children;
    const newChildren = n2.children;
    // 2.1 如果新的子节点是字符串类型
    if(typeof newChildren === 'string'){
        // 如果旧子节点也是字符串
        if(typeof oldChildren === 'string'){
            // 如果新旧节点内容不同,则将新值赋上
            if(newChildren !== oldChildren){
                el.textContent = newChildren;
            }
        }else{
            // 旧的子节点不是字符串,新的是字符串,直接替换
            el.textContent = newChildren;
        }
    }else{
    }
    

    其实新节点是字符串的时候比较好处理,不管旧节点是否为字符串,最后都把新节点内容替换到 textContent 上即可。条件在于新旧节点内容是否发生变化。具体细节在代码注释里都写得比较清楚,这里就不再赘述。

    接下来处理第二步(接上一步代码):

    // 2.处理子节点
    const oldChildren = n1.children;
    const newChildren = n2.children;
    // 2.1 如果新的子节点是字符串类型
    if(typeof newChildren === 'string'){
    ...
    }else{
      // 2.2 如果新的子节点不是字符串
      // 但是旧子节点是字符串
      if(typeof oldChildren === 'string'){
          el.innerHTML = ''; // 先把元素内容置空,用于遍历渲染新的子节点
          newChildren.forEach(child=>{
            mount(child,el); // 把子节点一个个渲染到el元素下
          })
      }else{
          // 2.3 新旧子节点都不是字符串
          const commonLength = Math.min(oldChildren.length,newChildren.length);
          // 对于所有公共节点,递归进行比较
          for(let i=0;i<commonLength;i++){
              patch(oldChildren[i],newChildren[i])
          }
          // 新节点更多,将多出来的节点添加
          if(newChildren.length > oldChildren.length){
              newChildren.slice(oldChildren.length).forEach(child=>{
                mount(child,el)
              })
          }
          // 旧节点更多,移除多出来的节点
          if(oldChildren.length > newChildren.length){
              oldChildren.slice(newChildren.length).forEach(child=>{
                el.removeChild(child.el)
              })
          }
      }
    }
    

    具体解读这段代码,新节点不是字符串,那就是一个数组,包含多个节点内容:

    • 旧节点为字符串:把当前节点内容 innerHTML 置空,遍历新节点内容调用 mount 把每个子节点渲染到 el 元素上。
    • 旧节点不是字符串,说明现在是两个数组对象的比较。
      • 首先取新旧子节点最小长度,先把他们共有的节点内容先处理了。具体是把公共元素递归调用 patch 再进行比较看每个子元素有何不同,多出来的子节点后面再处理;
      • 接着,如果新节点数量比较多,那么把多出来的子节点遍历添加到当前 el 下即可;
      • 如果新节点数量较少,则把旧节点多出来的内容移除掉。

    至此,所有的编译和渲染,以及节点更新都讲解完毕。当然,上述代码都是假设了比较理想的情况,但本着简单实现原则,希望大家通过阅读实践都能够对 Vue 框架内部基本原理有较为清晰的认知,那么这篇文章的目的也就达到了。

    最后,上述的所有代码我简单整理了一下放在 codePen 里,大家可以对照着看看。虽然功能不是很强大,虽然没有很高深的 diff 算法,但现在你能够很自信地说自己完全可以手写实现一个渲染框架了(即便很简陋,哈哈哈)。

    代码链接:手写实现 Vnode

    感谢阅读。

    题图来源:https://dribbble.com/mappleton


    起源地下载网 » 看得懂的 Vue 框架核心原理(上)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元