我的源码学习之路(六)---vue-2.6.14

190 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情

前言

今天是阳了的第八天,感觉还是晕晕乎乎,总感觉不舒服,又说不出来哪里不舒服的感觉。好好养着。


回顾

涉及的前端小知识【遇到就记录一下】

闭包

这个概念一直在各种场合困扰着我。我也不知道到底怎么去解释。每次我觉得我懂了但是每次遇到一些代码中的闭包时我又觉得跟我理解的不一样。对于这个概念写一个闭包,我也每次自己都是迷迷糊糊的。我希望这次我能搞懂并且能记住长的一段时间!!!

概念(各种各样的说法)

  • 通过一系列方法,将函数内部的变量(局部变量)转化为全局变量
  • 如果一个函数访问了此函数的父级及父级以上的作用域变量,就是闭包
  • 「函数」和「函数内部能访问到的变量」的总和,就是一个闭包。(函数套函数就是为了打造一个局部变量而已)。借鉴:「每日一题」JS 中的闭包是什么?
  • 闭包作用:间接访问一个变量

一般面试回答:

闭包就是内部函数可以访问外部函数中的变量。可以把函数内部跟外部联系起来。闭包可以把函数变量保存在内存中,不被回收

function name(){
    let i = 10 // 可以避免写成全局变量造成变量污染。局部变量会常驻在内存中
    function names(){ // 内部函数(闭包) 可以访问外部的变量:i 
        console.log(i)
    }
    return names()  // 把内部函数返回出去,使外部可以接收到
}

全局变量:在全局作用域下的变量,不可读取局部

局部变量:在局部函数下的变量,可以读取全局变量


vue常见的一些面试问题

 ## vue为什么需要key
 ## vue是如何进行diff的
 ## vue中slot实现及作用
 ## vue中watch和computed
 ## vue中$attrs实现及解决了什么问题
 ## vue中mixin使用场景和原理
 

vue为什么需要key

一般性回答:(大白话)

  • 为了vue在diff比较的时候进行节约成本,key相当于一个记号,如果记号一样,就说明没有改变,不用比较了。这样可以提高diff算法逻辑的效率
  • 不建议用index作为key!如果数组发生变化,index一直会保持不变,但是其内容是发生了改变的,还是要比较,这样使用的话实际上跟没写是一样的效果,起不到优化diff算法的效果。

用法举例:url(src/core/vdom/patch.js(35))


function sameVnode (a, b) { 判断是否是同一个节点
  return (
    a.key === b.key && //判断a, b节点的key是否相同
    a.asyncFactory === b.asyncFactory && (
      (
        a.tag === b.tag && // 标签是否都一样
        a.isComment === b.isComment && // 注释是否都一样
        isDef(a.data) === isDef(b.data) && //  data是否一样
        sameInputType(a, b) // input标签情况是否一样
      ) || (
        isTrue(a.isAsyncPlaceholder) && // 异步占位符节点
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

具体的源码举例实践和学习diff算法时一起记录

vue是如何进行diff的

一般回答:

  • patch算法有三个作用:负责首次渲染、后续更新、销毁组件
  • diff算法是个深度优先算法,时间复杂度O(n).具有高效性必要性,采取的是首尾交叉比较的方式

进入diff的过程:首先进入生命周期的代码[src/core/instance/lifecycle.js(190)]执行updateComponent方法,触发_update内部的__patch__

  • 这个__patch__是在[/src/platforms/web/runtime/index.js](Vue.prototype.patch = inBrowser ? patch : noop)中挂载到原型上的

  • 这个patch是在[/src/platforms/web/runtime/patch.js](export const patch: Function = createPatchFunction({ nodeOps, modules }))中返回的,而引入就是[/src/platforms/web/runtime/patch.js]

image.png

if (!prevVnode) { 如果不存在prevVnode
 // initial render (最初的渲染)
 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
 // updates(更新时期)
 vm.$el = vm.__patch__(prevVnode, vnode)
}

而我们patch(src/core/vdom/patch.js)比较的方法就是这个文件。现在来主要看一下逻辑顺序【本文件存在重要的diff算法】

文件内方法结构:入口方法就是createPatchFunction image.png

export function createPatchFunction (backend) {
    ...
    return function patch (oldVnode, vnode, hydrating, removeOnly) {
    //如果新节点不存在,老节点存在,调用destroyHook,销毁老节点
      if (isUndef(vnode)) {
        if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
        return
      }

      let isInitialPatch = false
      const insertedVnodeQueue = []

      if (isUndef(oldVnode)) {
        // 如果新的节点存在,旧节点不存在
        // empty mount (likely as component), create new root element
        // <div id="app"><comp></comp></div>
        // 这里的 comp 组件初次渲染时就会走这儿
        isInitialPatch = true
        createElm(vnode, insertedVnodeQueue) // 创建新节点
      } else {
        // 判断oldVnode是否为真实元素
        const isRealElement = isDef(oldVnode.nodeType)
        if (!isRealElement && sameVnode(oldVnode, vnode)) {
            // 如果不是真实元素且新旧节点是同一节点,则是更新阶段,执行diff
          // patch existing root node
          patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) // diff主要方法
        } else {
            // 是真实元素,标识初次渲染
          if (isRealElement) {
              //挂载到真实元素以及处理服务端渲染的情况
            if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
              oldVnode.removeAttribute(SSR_ATTR)
              hydrating = true
            }
            if (isTrue(hydrating)) {
              if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
                invokeInsertHook(vnode, insertedVnodeQueue, true)
                return oldVnode
              } else if (process.env.NODE_ENV !== 'production') {
                warn(
                  'The client-side rendered virtual DOM tree is not matching ' +
                  'server-rendered content. This is likely caused by incorrect ' +
                  'HTML markup, for example nesting block-level elements inside ' +
                  '<p>, or missing <tbody>. Bailing hydration and performing ' +
                  'full client-side render.'
                )
              }
            }
            // 根据oldVnode创建一个空的vnode节点
            oldVnode = emptyNodeAt(oldVnode)
          }

          // replacing existing element
          const oldElm = oldVnode.elm 获取旧节点的元素
          const parentElm = nodeOps.parentNode(oldElm) 获取老节点的父元素,即body

          // create new node 创建新节点。基于新vnode创建整颗DOM树并插到body中去
          createElm(
            vnode,
            insertedVnodeQueue,
            oldElm._leaveCb ? null : parentElm,
            nodeOps.nextSibling(oldElm)
          )
            // 递归更新父占位符节点元素
          if (isDef(vnode.parent)) {
            let ancestor = vnode.parent
            const patchable = isPatchable(vnode)
            while (ancestor) {
              for (let i = 0; i < cbs.destroy.length; ++i) {
                cbs.destroy[i](ancestor)
              }
              ancestor.elm = vnode.elm
              if (patchable) {
                for (let i = 0; i < cbs.create.length; ++i) {
                  cbs.create[i](emptyNode, ancestor)
                }
                const insert = ancestor.data.hook.insert
                if (insert.merged) {
                  // start at index 1 to avoid re-invoking component mounted hook
                  for (let i = 1; i < insert.fns.length; i++) {
                    insert.fns[i]()
                  }
                }
              } else {
                registerRef(ancestor)
              }
              ancestor = ancestor.parent
            }
          }

          // destroy old node 移除或者销毁旧节点
          if (isDef(parentElm)) {
            removeVnodes([oldVnode], 0, 0)
          } else if (isDef(oldVnode.tag)) {
            invokeDestroyHook(oldVnode)
          }
        }
      }

      invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
      return vnode.elm
    }
}
patchVnode主要介绍一下这个方法
function patchVnode (
  oldVnode, // 旧节点
  vnode, // 新节点
  insertedVnodeQueue, // 插入节点的队列
  ownerArray,// 节点数组
  index, // 当前节点的index
  removeOnly // 只有在 patch 函数中被传入,当老节点不是真实的 dom 节点,当新老节点是相同节点的时候
) {
  if (oldVnode === vnode) { // 如果新旧节点完全相等,直接返回
    return
  }

  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  const elm = vnode.elm = oldVnode.elm

  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
      vnode.isAsyncPlaceholder = true
    }
    return
  }

  // reuse element for static trees.
  // note we only do this if the vnode is cloned -
  // if the new node is not cloned it means the render functions have been
  // reset by the hot-reload-api and we need to do a proper re-render.
  if (isTrue(vnode.isStatic) && 新节点是静态节点
    isTrue(oldVnode.isStatic) && 旧节点是静态节点
    vnode.key === oldVnode.key && 新旧节点key一样
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))被标记了once的
  ) {
    vnode.componentInstance = oldVnode.componentInstance
    return
  }

  let i
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { 调用prepatch的钩子函数
    i(oldVnode, vnode)
  }

  const oldCh = oldVnode.children //旧节点的子节点
  const ch = vnode.children //新节点的子节点
  // 调用update钩子函数
  if (isDef(data) && isPatchable(vnode)) {
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
  }
  if (isUndef(vnode.text)) { // 新节点没有text属性
    if (isDef(oldCh) && isDef(ch)) {// 如果都有子节点,对比更新子节点
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) { //新节点在,老节点不在
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(ch)
      }
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')// 如果老节点是 text, 直接清空
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)// 新增子节点
    } else if (isDef(oldCh)) { //旧节点在,新节点不在
      removeVnodes(oldCh, 0, oldCh.length - 1) // 移除
    } else if (isDef(oldVnode.text)) { // 若旧节点是text,清空
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {// 若旧节点text !==新节点text
    nodeOps.setTextContent(elm, vnode.text) // 新的替换旧的
  }
  if (isDef(data)) {
  // 执行 postpatch 钩子函数
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  }
}

接下来进入到updateChildren

image.png

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  let oldStartIdx = 0 // 旧列表起点位置
  let newStartIdx = 0 // 新列表起点位置
  let oldEndIdx = oldCh.length - 1 // 旧列表终点位置
  let oldStartVnode = oldCh[0] // 旧列表起点值
  let oldEndVnode = oldCh[oldEndIdx] // 旧列表终点值
  let newEndIdx = newCh.length - 1 // 新列表终点位置
  let newStartVnode = newCh[0] // 新列表起点值
  let newEndVnode = newCh[newEndIdx] // 新列表终点值
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm
  const canMove = !removeOnly

  if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(newCh)
  }
// 最重要的一段:首尾比较【这一段用文字来使劲描述一下】
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  //前两个if:
  // 如果`oldStartVnode`不存在,`oldCh`起始点向后移动。如果`oldEndVnode`不存在,`oldCh`终止点向前移动。
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
    // 如果`oldStartVnode` 和 `newStartVnode` 是sameVnode,则`patchVnode`,同时彼此向后移动一位
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
    // 如果`oldEndVnode` 和 `newEndVnode` 是sameVnode,则`patchVnode`,同时彼此向前移动一位
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
    //如果`oldStartVnode` 和 `newEndVnode` 是 sameVnode,则先 `patchVnode`,然后把`oldStartVnode`移到`oldCh`最后的位置即可,然后`oldStartIdx`向后移动一位,`newEndIdx`向前移动一位
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
    //如果`oldEndVnode` 和 `newStartVnode` 是 sameVnode,则先 `patchVnode`,然后把`oldEndVnode`移到`oldCh`最前的位置即可,然后`newStartIdx`向后移动一位,`oldEndIdx`向前移动一位
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
    //如果没有相同的 key,执行 createElm 方法创建元素。
      if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (isUndef(idxInOld)) { // New element
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
      } else {
      //如果有相同的 key,就判断这两个节点是否为sameNode
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          // same key but different element. treat as new element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        }
      }
      newStartVnode = newCh[++newStartIdx]
    }
  }
  if (oldStartIdx > oldEndIdx) { //若旧列表起点 大于 旧列表末尾,直接添加
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  } else if (newStartIdx > newEndIdx) {//若新列表起点 大于 新列表末尾,直接移除
    removeVnodes(oldCh, oldStartIdx, oldEndIdx)
  }
}

根据上面的代码逻辑,画一画图,肯定能更明晰

Desktop 1.png

vue中slot实现及作用

slot插槽:可以在父组件书写代码段,嵌入到子组件指定位置上

但是为啥父组件内写的内容会被定义在子组件内呢?这就是想要探讨的位置。

原因:就是子组件定义了插槽,而插槽定义是在我们源码中组件初始化的时候initRender()中就出现了src/core/instance/render.js

image.png

一般在vue项目中开发写slot,我们都需要使用template来承接

<template v-slot:header>
</template>

resolveSlots(children,context) 接收两个参数

children[vnode]:

image.png

context: image.png

vue中watch和computed

一般回答:

watch和computed都是vue中用来监听数据的属性。但是两者也有一定的区别

watch:监听的数据为data里面的数据,在data里面的数据发生变化的时候执行一些操作。可以当数据发生变化的时候执行异步操作或者开销较大的操作。

computed:叫做计算属性,其内部的方法名像data中的数据一样以属性的方式访问。computed内部的方法是基于data中的数据的加工计算得出来的。有缓存功能,如果方法中依赖的数据没有发生改变的话,不会重新计算。有复杂计算的时候可以使用计算属性:过滤,遍历,累计等复杂的计算

源码上:

src/core/instance/state.js image.png 通过观察源码看这两个方法:内部的原理都是调用了watcher的实例方法+响应式中的get、set方法。---具体的源码代码也在前面梳理过,这里就是大概记录一下两者的区别和相同点

vue中 $attrs及listeners 实现及解决了什么问题

一般回答: attrs:接收除了props生命外的绑定的属性(除class,style),(如果给组件传递的数据,组件不使用props接收,那么这些数据将作为组件的HTML元素的特性,这些特性绑定在组件的HTML根元素上)

源码中: 出现位置:src/core/instance/init.js(54)initRender[src/core/instance/render.js](44)方法

image.png

用法

可用于组件通信。

  • attrs: 包含了父作用域中不作为prop被识别的特性绑定(除class,style)

image.png

image.png

vue中mixin使用场景和原理

一般回答:

   vue中的`混入`命令,可以灵活地分发组件中的可复用功能。分为两种情况:全局mixin和基础全局options混入。
   mixin混入后的执行顺序:组件自己的,其他文件混入的,全局的
   全局的混入也会影响第三方插件

源码中src/core/index.js中的initGlobalAPI(Vue)--initMixin(Vue)

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

每个组件在初始化的时候都会生成一个vm,在创建组件实例前,要把全局的options 渗透到 每个组件中。主要的方法就是mergeOptions

export function mergeOptions (
  parent: Object, // this.options
  child: Object, // Vue
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)


  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) { //key:data, hook, props, methods, injuect, computed等等
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

Vue中有几种合并策略:

替换:props methods inject computed 中同名的会被后来的替代掉
合并: data 会将本组件的、混合的合并在一起,递归合并。
export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    // in a Vue.extend merge, both should be functions
    if (!childVal) { // 如果child 没有 就直接用parent
      return parentVal
    }
    if (!parentVal) {// 如果parent 没有 就直接用child
      return childVal
    }
  
    return function mergedDataFn () {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      )
    }
  } else {
    return function mergedInstanceDataFn () {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        return mergeData(instanceData, defaultData) // 递归合并当前的对象
      } else {
        return defaultData
      }
    }
  }
}
队列: hooks watch,会被合并成一个数组,然后正序遍历一次执行
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}

function dedupeHooks (hooks) {
  const res = []
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i])
    }
  }
  return res
}
叠加:component directives filters: 主要是通过原型链进行层层叠加

后记

本文仅作为自己一个阅读记录,具体还是要看大佬们的文章

感觉整体看下来,源码也不是一个很难懂的东西。

下一篇:我的源码学习之路(七)---vue-2.6.14