vue源码阅读之render和_update做了什么?

从响应式原理一文了解到Vue最终是通过updateComponent方法来更新视图,updateComponent函数是调用 vm._update(vm._render(), hydrating)方法,_render内部是调用 vm.$options.render 方法 ;那我们就来了解下update函数和render函数到底做了什么。

render

我们在用vue-cli的时候main.js通常会有这么一段代码

// Vue实例化得参数就是 vm.$options 
new Vue({
  router,
  i18n,
  render: h => h(App)
}).$mount('#app');
复制代码

通过这里可以看出 render 是一个函数 他接受一个参数h,h也是一个函数,那h究竟是什么?h的本质其实就是createElement函数

createElement

作用

返回要创建的dom元素的描述,不是一个真实的 DOM 元素,它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也就是vnode,由vnode组成的映射真实dom的树我们称为‘虚拟DOM’。

总结:createElement 函数的作用是返回虚拟节点。

参数 (3 个)

createElement (renderElement: String | Component, definition: Object, children: String | Array)

/*
* params 参数名自定义的 跟源码有差入 
renderElement : 必填项 一个 HTML 标签名、组件选项对象,或者resolve 了上述任何一种的一个 async 函数。
definition: 可填项 与模板中 attribute 对应的 数据对象。
children: 可填项 子虚拟节点 (VNodes),也是由 `createElement()` 构建而成,也可以使用字符串来生成“文本虚拟节点”。
*/
复制代码

使用场景

// 什么情况我们可以使用render函数去替换模板?

// 假设有如下场景 根据等级 level 动态生成 h: level 的模板内容
<div>
  // level 1 : h1 、 level2 : h2 、 level3 : h3
  <h1>
    // 内容部分
    {{content}}
  </h1>
</div>

// --------------------------------分割线--------------------------------------
// 如果用模板template生成,页面就是以下这个样子 TestTemplate.vue

<template>
  <h1 v-if='level === 1'>
     <slot></slot>
  </h1>
  
  <h2 v-eles-if='level === 2'>
     <slot></slot>
  </h2>
  
  <h3 v-eles-if='level === 3'>
     <slot></slot>
  </h3>
  
  ......
</template>


// 此时我们可以通过 以下代码生成上述场景
// <TestTemplate :level = '1'> 你好呀 </TestTemplate>
// 但是当等级过多的时候这个代码就很难看,很多余且重复书写了 <slot></slot>

// -------------------------------分割线------------------------------------
// 此时我们可以通过render方法来实现上述场景

<script>
  Vue.component('test-template', {
    render: function (createElement) {
      return createElement(
        'h' + this.level,   // 标签名称
        this.$slots.default // 子节点数组
      )
    },
    props: {
      level: {
        type: Number,
        required: true
      }
    }
  })
</script>

// 到这里会有一个疑问 ,就是createElement第二个参数是`数据对象definition`, 而这里传的是子节点数组,应该是第三个参数, 为什么会忽略第二个参数

// 解答:在createElement函数内部会进行第二个参数类型的判断,如果是数组或者非对象,就会给第二个参数换到第三个参数上去,第二个参数会给undefined。

// 源码位置: src\core\vdom\create-element.js
复制代码

深入数据对象(也就是createElement第二个参数到底可以定义啥东西)

// 官网示例
{
  // 与 `v-bind:class` 的 API 相同,
  // 接受一个字符串、对象或字符串和对象组成的数组
  'class': {
    foo: true,
    bar: false
  },
  // 与 `v-bind:style` 的 API 相同,
  // 接受一个字符串、对象,或对象组成的数组
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML attribute
  attrs: {
    id: 'foo'
  },
  // 组件 prop
  props: {
    myProp: 'bar'
  },
  // DOM property
  domProps: {
    innerHTML: 'baz'
  },
  // 事件监听器在 `on` 内,
  // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
  // 需要在处理函数中手动检查 keyCode。
  on: {
    click: this.clickHandler
  },
  // 仅用于组件,用于监听原生事件,而不是组件内部使用
  // `vm.$emit` 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
  // 赋值,因为 Vue 已经自动为你进行了同步。
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 作用域插槽的格式为
  // { name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 如果组件是其它组件的子组件,需为插槽指定名称
  slot: 'name-of-slot',
  // 其它特殊顶层 property
  key: 'myKey',
  ref: 'myRef',
  // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
  // 那么 `$refs.myRef` 会变成一个数组。
  refInFor: true
}
复制代码

render作用

render 函数作用就是使用Javascript创建模板,她的重点是createElement参数

在render函数中如何使用v-if、v-for、v-model、事件、描述符、插槽、jsx等可以去官网了解下。

-------------------------------官网传送门-----------------------------------

update

update顾名思义就是起到更新的作用,我们通过render已经得到vnode虚拟节点,那这个update方法他的作用肯定是将vnode组成的树也就是虚拟dom更新成真实dom的,那它内部是怎么实现的呢。

// _update 定义
// src\core\instance\lifecycle.js
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    if (!prevVnode) { 
      // 初始渲染
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else { 
      // 数据更新渲染
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    // 省略部分代码
  }
复制代码

可以看到 vm._update 是调用的vm.__patch__方法,这时候查询__patch__方法可以看到这方法在不同的平台,web 和 weex 上的定义是不一样的,在 web 平台中它的定义在 src\platforms\web\runtime\index.js 中

// __patch__ 方法定义
// src\platforms\web\runtime\index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
// 在不是浏览器环境下返回了个空函数,浏览器环境下调用patch方法
复制代码
// patch 方法定义
// src\platforms\web\runtime\patch.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'

const modules = platformModules.concat(baseModules)

// patch 实际是 createPatchFunction 函数 接受一个对象
// 对象传递俩参数 nodeOps :一些对dom的操作, modules应该是对不同平台的处理(eg:weex/web)
export const patch: Function = createPatchFunction({ nodeOps, modules })
复制代码

patch

// src\core\vdom\patch.js
export function createPatchFunction (backend) {
  let i, j
  const cbs = {}
  const { modules, nodeOps } = backend
  
  // ********省略辅助函数代码
  
  // 核心代码就是返回一个patch函数
  // 这个函数可以更新真实dom
  /*
   *params
    oldVnode 旧节点
    vnode render函数返回的虚拟节点
    hydrating 是否服务器端渲染
    removeOnly 看文章说是给transition-group用的,感兴趣的可以了解下
  */
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) { // 旧节点未定义
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue) // 直接创建
    } else {
      // 判断是否是真实dom, 第一次渲染时传入vm.$el是真实节点, 所以为true
      const isRealElement = isDef(oldVnode.nodeType) //nodeType:1 元素、3:文本,8:注释
      // 如果不是真实节点,且新旧节点相同(通过比较两个节点的 key、tag、注释节点、数据信息是否相等来判断两个 Node 节点是否是相同节点,对 input 标签做了一个单独的判断,为了兼容不同浏览器。)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // vnode的diff算法
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // 第一次渲染会进else,因为vm.$el不为空,传入的是真实节点。
        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.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)  // 创建一个空节点
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // 创建新节点
        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)
        )

        // 省略部分代码

        // 移除旧节点
        if (isDef(parentElm)) { // 第一次渲染时parentElm节点不为空,所以要移除。
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}
复制代码

初次渲染时断点截图,执行完createElm还未执行removeVnodes

patch 作用

通过观察patch函数可以得知,他是通过对比oldVnode和vnode差异,映射到真实dom上,具体diff算法比较复杂,我就不深究了,感兴趣的jym可以了解一下。

总结

学习到这里,再结合上篇文章 vue源码阅读之响应式原理 ,我们可以了解到 data 是如何触发 view 更新的

  1. defineReactive 中通过 Object.defineProperty 使 data数据响应式;

  2. Depgetter 中作依赖收集,在 setter 中派发更新;

  3. 通过 dep.notify() 通知 Watcher 更新,最终调用 vm._update(vm._render(), hydrating) 更新 UI;

  4. view 触发 data 更新就是通过事件方法触发datasetter,从而实现双向绑定

猜你喜欢

转载自juejin.im/post/7083332802527100935