Vue2源码梳理:update的整体实现流程

update

  • 回到之间 $mount时,mountComponent 函数的过程,vm._render 是如何创建了一个 VNode

  • 接下来就是要把这个 VNode 渲染成一个真实的 DOM 并渲染出来,这个过程是通过 vm._update 完成的

  • _update 它是一个vue 的私有方法,它把我们的 vnode 渲染成真实的 dom

  • _update的方法,也是原型上的一个方法, 它的定义在 src/core/instance/lifecycle.js 中

    js 复制代码
    export function lifecycleMixin (Vue: Class<Component>) {
      Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
        // 首先它定义一些变量,这些变量实际上都是给之后数据更新时候用的
        // 因为update它的调用实际有两个,一个是我们首次渲染的时候,它会调用update把我们的vnode映射是真实的dom
        // 还有一个就是当我们去改变数据的时候,数据改变也会驱动视图的变化,它同样也会调update方法
        // 可以初步认为它们都是空
        const vm: Component = this
        const prevEl = vm.$el
        const prevVnode = vm._vnode
        const restoreActiveInstance = setActiveInstance(vm)
        vm._vnode = vnode
        // Vue.prototype.__patch__ is injected in entry points
        // based on the rendering backend used.
        // 这个 prevVnode 也是空,它会走到这个逻辑,就是 initial render,否则会走到 updates
        if (!prevVnode) {
          // initial render
          vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
        } else {
          // updates
          vm.$el = vm.__patch__(prevVnode, vnode)
        }
        restoreActiveInstance()
        // update __vue__ reference
        if (prevEl) {
          prevEl.__vue__ = null
        }
        if (vm.$el) {
          vm.$el.__vue__ = vm
        }
        // if parent is an HOC, update its $el as well
        if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
          vm.$parent.$el = vm.$el
        }
        // updated hook is called by the scheduler to ensure that children are
        // updated in a parent's updated hook.
      }
      // ...
    }
    • 这里 vm.__patch__ 定义在 src/platforms/web/runtime/index.js 中

      js 复制代码
      Vue.prototype.__patch__ = inBrowser ? patch : noop
      • 这里浏览器环境,才会有 patch, 这里的 inBrowser

        js 复制代码
        // src/core/util/env.js
        export const inBrowser = typeof window !== 'undefined'
      • vue它是可以跑在服务端的,在服务端的 patch 是一个空函数,因为在服务端不会有dom

    • 进入 patch, 定义在 src/platforms/web/runtime/patch.js

      js 复制代码
      import * as nodeOps from 'web/runtime/node-ops'
      import { createPatchFunction } from 'core/vdom/patch'
      import baseModules from 'core/vdom/modules/index'
      import platformModules from 'web/runtime/modules/index'
      
      // the directive module should be applied last, after all
      // built-in modules have been applied.
      const modules = platformModules.concat(baseModules)
      
      // 这个 patch 方法,调用了这个 createPatchFunction, 返回来是一个函数,特别像高阶函数这样的概念
      // nodeOps 是操作dom的集合方法;而 modules 是 platformModules 和 baseModules,这里定义属性,类和钩子函数等
      
      // 这里才是真正的 patch 函数,vue饶了一个大圈子,为什么会定义在这里
      // 他利用函数柯里化的技巧,一开始传入 nodeOps, modules 都是和平台相关的
      // vue 是跨平台的,可以在node上跑,可以在浏览器上跑,可以在 weex 上
      // 在web上真实操作dom的api 和 weex 上是不同的,虽然最终可以生成同样的 vnode
      // 这块是分离的, 所以 dom操作的api通过参数来传入
      
      // 另外就是 models 模块,它的一些生命周期,在不同的生命周期要做的事情,然后怎么去生成这些模块和我们dom做配合, 这些东西,跟平台也是相关的
      // 这里有 baseModules 和 platformModules
      // 通过函数柯里化,可基于不同参数来干不同的事情,把写代码的差异去搞定
      // 等到真实执行 patch 的时候,就不用考虑平台的区别了
      // 因为我们是把平台差异化的东西都已经在之前抹平了
      
      // 所以这样的话,就可以每次调用这个 patch 的时候,完全不用关心真实操作是什么
      // 因为在之前已经把这个参数提前传入了,相当于通过这种闭包的技巧,实现了对 nodeOps 和 modules 的持有
      // 接下来真实调每次的方法的时候,都不用再去传这些差异化的参数,这就是一个非常好的一个js的编程技巧
      export const patch: Function = createPatchFunction({ nodeOps, modules })
      • 进入 createPatchFunction, 定义在 src/core/vdom/patch.js

        js 复制代码
        // 在patch阶段中,会执行各个钩子
        const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
        
        export function createPatchFunction (backend) {
          let i, j
          const cbs = {}
        
          // 拿到 这两项
          const { modules, nodeOps } = backend
        
          // 将 hooks 遍历到 cbs 对象中保存
          for (i = 0; i < hooks.length; ++i) {
            cbs[hooks[i]] = []
            // 遍历所有模块,拿到所有模块的钩子
            for (j = 0; j < modules.length; ++j) {
              if (isDef(modules[j][hooks[i]])) {
                cbs[hooks[i]].push(modules[j][hooks[i]])
              }
            }
          }
        
          // ... 这里定义了非常多的辅助函数
        
          // 这个patch实际上是传入了四个参数
          // 关注前两个,oldVnode 是 真实的el, vnode 就是vdom, 后面两个都是 false
          return function patch (oldVnode, vnode, hydrating, removeOnly) {
            // 销毁时 return 跳过
            if (isUndef(vnode)) {
              if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
              return
            }
        
            let isInitialPatch = false
            const insertedVnodeQueue = []
        
            // 这个 if 一般不会走到,跳过
            if (isUndef(oldVnode)) {
              // empty mount (likely as component), create new root element
              isInitialPatch = true
              createElm(vnode, insertedVnodeQueue)
            } else {
              // 这里,判断是否是真实的dom,这里是 true
              const isRealElement = isDef(oldVnode.nodeType)
              if (!isRealElement && sameVnode(oldVnode, vnode)) {
                // patch existing root node
                patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
              } else {
                // 这里执行
                if (isRealElement) {
                  // mounting to a real element
                  // check if this is server-rendered content and if we can perform
                  // a successful hydration.
                  // 这里不是 ssr, 这里走不到
                  if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
                    oldVnode.removeAttribute(SSR_ATTR)
                    hydrating = true
                  }
                  // 这里浏览器dom环境下,也走不到
                  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.'
                      )
                    }
                  }
                  // either not server-rendered, or hydration failed.
                  // create an empty node and replace it
                  oldVnode = emptyNodeAt(oldVnode) // 这个方法就是把真实的dom转化为 vnode
                }
        
                // replacing existing element
                const oldElm = oldVnode.elm
                const parentElm = nodeOps.parentNode(oldElm) // 获取父节点
        
                // 这个函数很重要,把vnode挂载到真实dom上
                // create new node
                createElm(
                  vnode,
                  insertedVnodeQueue,
                  // extremely rare edge case: do not insert if old element is in a
                  // leaving transition. Only happens when combining transition +
                  // keep-alive + HOCs. (#4590)
                  oldElm._leaveCb ? null : parentElm,
                  nodeOps.nextSibling(oldElm)
                )
        
                // update parent placeholder node element, recursively
                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)
                      }
                      // #6513
                      // invoke insert hooks that may have been merged by create hooks.
                      // e.g. for directives that uses the "inserted" hook.
                      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(parentElm, [oldVnode], 0, 0) // 删除旧的节点
                } else if (isDef(oldVnode.tag)) {
                  invokeDestroyHook(oldVnode)
                }
              }
            }
        
            invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
            return vnode.elm
          }
        }
        • 进入 createElm

          js 复制代码
          function createElm (
            vnode,
            insertedVnodeQueue,
            parentElm,
            refElm,
            nested,
            ownerArray,
            index
          ) {
            // 这个一版不会走到,跳过
            if (isDef(vnode.elm) && isDef(ownerArray)) {
              // This vnode was used in a previous render!
              // now it's used as a new node, overwriting its elm would cause
              // potential patch errors down the road when it's used as an insertion
              // reference node. Instead, we clone the node on-demand before creating
              // associated DOM element for it.
              vnode = ownerArray[index] = cloneVNode(vnode)
            }
          
            vnode.isRootInsert = !nested // for transition enter check
            if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
              return
            }
          
            const data = vnode.data
            const children = vnode.children
            const tag = vnode.tag
            if (isDef(tag)) {
              if (process.env.NODE_ENV !== 'production') {
                if (data && data.pre) {
                  creatingElmInVPre++
                }
                // 这种错误是没有注册组件,却使用组件
                if (isUnknownElement(vnode, creatingElmInVPre)) {
                  warn(
                    'Unknown custom element: <' + tag + '> - did you ' +
                    'register the component correctly? For recursive components, ' +
                    'make sure to provide the "name" option.',
                    vnode.context
                  )
                }
              }
              // 这里生成原生dom节点
              vnode.elm = vnode.ns
                ? nodeOps.createElementNS(vnode.ns, tag)
                : nodeOps.createElement(tag, vnode)
              setScope(vnode)
              
              // 跳过
              /* istanbul ignore if */
              if (__WEEX__) {
                // ...
              } else {
                // 创建子节点
                createChildren(vnode, children, insertedVnodeQueue)
                if (isDef(data)) {
                  invokeCreateHooks(vnode, insertedVnodeQueue)
                }
                insert(parentElm, vnode.elm, refElm)
              }
          
              if (process.env.NODE_ENV !== 'production' && data && data.pre) {
                creatingElmInVPre--
              }
            } else if (isTrue(vnode.isComment)) {
              vnode.elm = nodeOps.createComment(vnode.text)
              insert(parentElm, vnode.elm, refElm)
            } else {
              vnode.elm = nodeOps.createTextNode(vnode.text)
              insert(parentElm, vnode.elm, refElm)
            }
          }
          • createElm 的作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中

          • 这里的 createComponent 方法目的是尝试创建子组件,在当前这个 case 下它的返回值为 false

          • 接下来判断 vnode 是否包含 tag,如果包含,先简单对 tag 的合法性在非生产环境下做校验,看是否是一个合法标签

          • 然后再去调用平台 DOM 的操作去创建一个占位符元素

            js 复制代码
            vnode.elm = vnode.ns
              ? nodeOps.createElementNS(vnode.ns, tag)
              : nodeOps.createElement(tag, vnode)
          • 进入 createChildren

            js 复制代码
            // createChildren(vnode, children, insertedVnodeQueue)
            function createChildren (vnode, children, insertedVnodeQueue) {
              if (Array.isArray(children)) {
                // 在 dev 环境对节点 key 做校验
                if (process.env.NODE_ENV !== 'production') {
                  checkDuplicateKeys(children)
                }
                for (let i = 0; i < children.length; ++i) {
                  createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
                }
              } else if (isPrimitive(vnode.text)) {
                nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
              }
            }
            • 如果是array, 遍历children 递归调用createElm
            • 其实 createChildren 很简单,如果 这个 vnode 还有子节点
            • 就递归调用子节点的 createElm, 一层层去处理
          • 接着再调用 invokeCreateHooks 方法执行所有的 create 的钩子并把 vnode push 到 insertedVnodeQueue 中

            js 复制代码
            if (isDef(data)) {
              invokeCreateHooks(vnode, insertedVnodeQueue)
            }
            
            function invokeCreateHooks (vnode, insertedVnodeQueue) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, vnode)
              }
              i = vnode.data.hook // Reuse variable
              if (isDef(i)) {
                if (isDef(i.create)) i.create(emptyNode, vnode)
                if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
              }
            }
          • 最终会调用 insert 操作, 定义在 src/core/vdom/patch.js

          • insert 方法把 DOM 插入到父节点中,因为是递归调用

          • 子元素会优先调用 insert,所以整个 vnode 树节点的插入顺序是先子后父

            js 复制代码
            insert(parentElm, vnode.elm, refElm)
            
            function insert (parent, elm, ref) {
              if (isDef(parent)) {
                if (isDef(ref)) {
                  if (ref.parentNode === parent) {
                    nodeOps.insertBefore(parent, elm, ref)
                  }
                } else {
                  nodeOps.appendChild(parent, elm)
                }
              }
            }
            
            // src/platforms/web/runtime/node-ops.js
            export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
              parentNode.insertBefore(newNode, referenceNode)
            }
            export function appendChild (node: Node, child: Node) {
              node.appendChild(child)
            }
            • 其实就是调用原生 DOM 的 API 进行 DOM 操作
  • 以上,我们可知,update 操作就是 patch 操作,里面的细节其实是非常多的

  • 这里只是梳理整体的调用流程

  • 从 new Vue 到渲染dom到页面的大致过程如下

  • new Vue -> init -> $mount -> compile(可选,存在编译版本) -> render -> vnode -> patch -> dom

相关推荐
赵大仁21 分钟前
深入理解 Vue 3 中的具名插槽
前端·javascript·vue.js·react.js·前端框架·ecmascript·html5
一雨方知深秋26 分钟前
v-bind 操作 class(对象,数组),v-bind 操作 style
前端·css·vue.js·html·style·class·v-bind
安晴晚风2 小时前
从0开始在linux服务器上部署SpringBoot和Vue
linux·运维·前端·数据库·后端·运维开发
前端小小王3 小时前
pnpm、Yarn 和 npm 的区别?
前端·npm·node.js
supermapsupport3 小时前
使用npm包的工程如何引入mapboxgl-enhance/maplibre-gl-enhance扩展包
前端·webpack·npm·supermap·mapboxgl
牛奔3 小时前
windows nvm 切换node版本后,npm找不到
前端·windows·npm·node.js
鱼大大博客3 小时前
Edge SCDN酷盾安全重塑高效安全内容分发新生态
前端·安全·edge
鸭梨山大。3 小时前
NPM组件包 vant部分版本内嵌挖矿代码
前端·安全·npm·node.js·vue
蟾宫曲8 小时前
在 Vue3 项目中实现计时器组件的使用(Vite+Vue3+Node+npm+Element-plus,附测试代码)
前端·npm·vue3·vite·element-plus·计时器
秋雨凉人心8 小时前
简单发布一个npm包
前端·javascript·webpack·npm·node.js