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

HTML如何渲染到浏览器

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

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

虚拟DOM

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

vue中三大核心系统

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

手动实现一些功能

了解了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);
      
      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])
          }
        }
      }
      
      if (vNode.children) {
        if (typeof vNode.children === 'string') { 
          el.innerHTML = vNode.children
        } else {
          for (let i in vNode.children) {
            mount(vNode.children[i], el)
          }
        }
      }
      
      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) {
      
      const el = n2.el = n1.el;
      if (n1.tag !== n2.tag) { 
        n1.el.parentElement.removeChild(n1.el)
        mount(n2, n1.el.parentElement)
      } else { 
        
        for (let key in n2.props) {
          const oldProp = n1.props[key];
          const newProp = n2.props[key]
          if (oldProp !== newProp) { 
            if (key.startsWith("on")) { 
              el.addEventListener(key.slice(2).toLowerCase(), newProp)
            } else { 
              el.setAttribute(key, newProp)
            }
          }
        }

        
        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)) { 
            el.removeAttribute(key);
          }
        }

        const oldChildren = n1.children || []
        const newChildren = n2.children || []
        
        if (typeof newChildren === 'string') { 
          if (typeof oldChildren === 'string') {
            if (!(oldChildren === newChildren)) { 
              el.innerHTML = newChildren
            }
          } else { 
            el.innerHTML = newChildren
          }
        } else { 
          if (typeof oldChildren === 'string') { 
            
            el.innerHTML = ''
            for (let vNode in newChildren) {
              mount(vNode, el)
            }
          } else { 
            
            
            
            
            
            const minChildrenLength = Math.min(oldChildren.length, newChildren.length)
            for (let i in minChildrenLength) {
              patch(oldChildren[i], newChildren[i])
            }

            if (oldChildren.length > minChildrenLength) {
              
              oldChildren.slice(minChildrenLength).forEach(vNode => {
                el.removeChild(vNode.el)
              })
            }

            if (newChildren.length > minChildrenLength) {
              
              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;
    }


    
    
    const targetMap = new WeakMap();
    function getDep(target, key) {
      
      let depsMap = targetMap.get(target);
      if (!depsMap) {
        depsMap = new Map();
        targetMap.set(target, depsMap);
      }

      
      let dep = depsMap.get(key);
      if (!dep) {
        dep = new Dep();
        depsMap.set(key, dep);
      }
      return dep;
    }


    
    function reactive(raw) {
      Object.keys(raw).forEach(key => {
        const dep = getDep(raw, key);
        let value = raw[key];

        Object.defineProperty(raw, key, {
          get() {
            
            dep.depend();
            return value;
          },
          set(newValue) {
            if (value !== newValue) {
              value = newValue;
              
              dep.notify();
            }
          }
        })
      })

      return raw;
    }

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

    
    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 操作符的捕捉器,等等其他操作。

  • 17
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Web面试那些事儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值