vue 源码学习笔记三 vue中如何生成虚拟DOM

发布时间:2019-08-19 发布网站:脚本宝典
脚本宝典收集整理的这篇文章主要介绍了vue 源码学习笔记三 vue中如何生成虚拟DOM脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。

vm._render 生成虚拟dom

我们知道在挂载过程中$mount 会调用 vm._update和vm._render 方法,vm._updata是负责把VNode渲染成真正的DOMvm._render方法是用来把实例渲染成VNode,这里的_render是实例的私有方法,和前面我们说的vm.render不是同一个,先来看下vm._render定义,vm._render是通过renderMixin(Vue)挂载的,定义在src/core/instance/render.js

// 简化版本
Vue.PRototyPE._render = function (): VNode {
  const vm: component = this
  const { render, _parentVnode } = vm.$options
  ...
  // render self
  let vnode
  try {
    // _renderProxy生产环境下是vm
    // 开发环境可能是proxy对象
    vnode = render.call(vm._renderProxy, vm.$createElement) // 近似vm.render(createElement)
  } catch (e) {...}
  // return empty vnode in case the render function errored out
  if (!(vnode instanceof VNode)) {
    if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {...}
    vnode = createEmptyVNode()
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}
@H_406_83@
  • 先缓存vm.$options.rendervm.$options._parentVnodevm.$options.render是在上节的$mount中通过comileToFunctions方法将template/el编译来的。
  • vnode = render.call(vm._renderProxy, vm.$createElement)调用了render方法,参数是vm._renderProxy,vm.$createElement
  • 拿到vnode后,判断类型是否为VNode,如果有多个vnode,则是模板上有多个根节点,触发告警。
  • 挂载vnode父节点,最后返回vnode
  • 简要概括,vm._render函数最后是通过render执行了createElement方法并返回vnode;下面就来具体看下vm._renderProxy,vm.$createElement,vnode

    vm._renderProxy

    首先来看下vm._renderProxyvm._renderProxy是在_inIT()中挂载的:

    Vue.prototype._init = function (options?: Object) {
      ...
      if (process.env.NODE_ENV !== 'production') {
        // 对vm对一层拦截处理,当使用vm上没有的属性时将告警      
        initProxy(vm)
      } else {
        vm._renderProxy = vm
      }
      ...
    }

    如果是生产环境,vm._renderProxy直接就是vm;开发环境下,执行initProxy(vm),找到定义:

    initProxy = function initProxy (vm) {
      if (hasProxy) {
        // determine which proxy handler to use
        const options = vm.$options
        const handlers = options.render && options.render._withStripped
          ? getHandler
          : hasHandler
        // 对vm对一层拦截处理
        vm._renderProxy = new Proxy(vm, handlers)
      } else {
        vm._renderProxy = vm
      }
    }

    先判断当前是否支持Proxy(ES6新语法),支持的话会实例化一个Proxy, 当前例子用的是hasHandler(只要判断是否vm上有无属性即可),这样每次通过vm._renderProxy访问vm时,都必须经过这层代理:

    // 判断对象是否有某个属性
    const hasHandler = {
      has (target, key) {
        // vm中是否有key属性
        const has = key in target
        // 当key是全局变量或者key是私有属性且key没有在$data中,允许访问该key
        const isAllowed = allowedGlobals(key) ||
          (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
        // 没有该属性且不允许访问该属性时发起警告
        if (!has && !isAllowed) {
          if (key in target.$data) warnReservedPrefix(target, key)
          else warnNonPresent(target, key)
        }
        return has || !isAllowed
      }
    }

    所以,_render中的vnode = render.call(vm._renderProxy, vm.$createElement),实际上是执行vm._renderProxy.render(vm.$createElement)

    Virtual DOM 虚拟dom

    vue.2.0中引入了virtual dom ,大大提升了代码的性能。所谓virtual dom ,就是用js对象去描述一个dom节点,这比真实创建dom快很多。在vue中,Virtual dom是用类vnode来表示,vnodesrc/core/vdom/vnode.js中定义,有真实dom上也有的属性,像tag/text/key/data/children等,还有些是vue的特色属性,在渲染过程也会用到.

    vm.$createElement

    vue文档中介绍了render函数,第一个参数就是createElement,之前的例子转换成render函数就是:

    <div id="app">
      {{ message }}
    </div>
    // 转换成render:
    render: function (createElement) {
      return createElement('div', {
         attrs: {
            id: 'app'
          },
      }, this.message)
    }

    可以看出,createElement就是vm.$createElement

    找到vm.$createElement定义,在initRender方法中,

    // bind the createElement fn to this instance
    // so that we get proper render context inside it.
    // args order: tag, data, children, normalizationType, alwaysNormalize
    // internal version is used by render functions compiled From templates
    vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
    // normalization is always applied for the public version, used in
    // user-written render functions. 
    vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

    看到这里定义了2个实例方法都是调用的createElement,一个是用于编译生成的render方法,一个是用于手写render方法,createElement最后会返回Vnode,来看下createElement的定义:

    export function createElement (
      context: Component, //vm实例
      tag: any,
      data: any, //可以不传
      children: any,// 子节点
      normalizationType: any,
      alwaysNormalize: boolean
    ) {
      // 参数判断,不传data时,要把children,normalizationType参数往前移
      if (Array.isArray(data) || isPrimitive(data)) {
        normalizationType = children
        children = data
        data = undefined
      }
      if (isTrue(alwaysNormalize)) {
        normalizationType = ALWAYS_NORMALIZE
      }
      return _createElement(context, tag, data, children, normalizationType)
    }

    先经过参数重载,根据alwaysNormalize传不同的normalizationType,调用_createElement(),实际上createElement是提前对参数做了一层处理
    这里的参数重载有个小点值得注意,normalizationType是关系到后面children的扁平处理,没有children则不需要对normalizationType赋值,childrennormalizationType就都是空值

    _createElement()

    1. 首先校验data,data是响应式的,调用createEmptyVNode直接返回注释节点:
    export const createEmptyVNode = (text: string = '') => {
      const node = new VNode()
      node.text = text
      node.isComment = true//注释vnode
      return node
    }
    1. 处理tag,没有tag时也返回注释节点
    2. key做基础类型校验
    3. children中有function类型作slot处理,此处先不作分析
    4. childrennormalize 变成vnode一维数组,有2种不同的方式:normalizeChildrensimpleNormalizeChildren
    5. 创建vnode

    simpleNormalizeChildren

    normalizeChildrensimpleNormalizeChildren是2种对children扁平化处理的方法,先来看下simpleNormalizeChildren定义:

    export function simpleNormalizeChildren (children: any) {
      for (let i = 0; i < children.length; i++) {
        // 把嵌套数组拍平成一维数
        if (Array.isArray(children[i])) {
          return Array.prototype.concat.apply([], children)
        }
      }
      return children
    }

    如果chilren中有一个是数组则将整个children作为参数组用concat连接,可以得到每个子元素都是vnodechildren,这适用于只有一级嵌套数组的情况

    normalizeChildren

    export function normalizeChildren (children: any): ?Array<VNode> {
      // 判断是否基础类型,是:创建文本节点,否:判断是否数组,是:作normalizeArrayChildren处理
      return isPrimitive(children)
        ? [createTextVNode(children)]
        : Array.isArray(children)
          ? normalizeArrayChildren(children)
          : undefined
    }

    普通的children处理:最后也是返回一组一维vnode的数组,当children是Array时,执行normalizeArrayChildren

    normalizeArrayChildren

    代码较长,此处就不贴了,可以自己对照码来分析:

    • 定义res
    • 遍历children,当children[i]是空或者是布尔值,跳过该次循环
    • 如果children[i]还是个数组,再对children[i]作normalizeArrayChildren处理

      if (Array.isArray(c)) {
        if (c.length > 0) {
          c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)// 返回vnode数组
          // merge adjacent text nodes 
          // 优化:如果c的第一个vnode和children上一次处理的vnode都是文本节点可以合并成一个vnode
          if (isTextNode(c[0]) && isTextNode(last)) {
            res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
            c.shift()
          }
          res.push.apply(res, c)
        }
      } else if (){...}
    • children[i]是基础类型时

      } else if (isPrimitive(c)) {
        // 当c是基础类型时
        // children上一次处理的vnode是文本节点,则合并成一个文本节点
        if (isTextNode(last)) {
          // merge adjacent text nodes
          // this is necessary for SSR hydration because text nodes are
          // essentially merged when rendered to HTML strings
          // 这是SSR hydration所必需的,因为文本节点渲染成html时基本上都是合并的
          res[lastIndex] = createTextVNode(last.text + c)
        } else if (c !== '') {
          // convert primitive to vnode
          res.push(createTextVNode(c))// c不为空直接创建文本节点
        }
      } else {
    • 其它情况,children[i]是vnode时,

      } else {// 当c是vnode时
        if (isTextNode(c) && isTextNode(last)) {
          // merge adjacent text nodes
          res[lastIndex] = createTextVNode(last.text + c.text)
        } else {
          // default key for nested array children (likely generated by v-for)
          // 特殊处理,先略过
          if (isTrue(children._isVList) &&
            isDef(c.tag) &&
            isUndef(c.key) &&
            isDef(nestedIndex)) {
            c.key = `__vlist${nestedIndex}_${i}__`
          }
          // push到res上
          res.push(c)
        }
      }
    • 最后返回一组vnode

    主要有2个点,一是normalizeArrayChildren递归调用,二是文本节点的合并

    创建vnode

    1. 创建vnode,并返回
    • 判断tag类型,为字符串时:

      let Ctor
      ns = (context.$vnode && context.$vnode.ns) || config.getTagnamespace(tag)
      // 判断tag是否是原生标签
      if (config.isReservedTag(tag)) {
        // platform built-in elements
        vnode = new VNode(
          config.parsePlatformtagName(tag), data, children,
          undefined, undefined, context
        )
      } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
        // component组件部分先略过
        vnode = createComponent(Ctor, data, context, children, tag)
      } else {
        // unknown or unlisted namespaced elements
        // check at runtime because it may get assigned a namespace when its
        // parent normalizes children
        // 未知标签,创建vnode
        vnode = new VNode(
          tag, data, children,
          undefined, undefined, context
        )
      }
      
      • tag不是字符串类型时,vnode = createComponent(tag, data, context, children),先略过
      • 最后再对生成的vnode作校验,返回vnode

    小结

    到此为止,我们分析了vm._render方法和_createElement方法,知道了创建vnode的整个过程,在$mount中的 vm._update(vm._render(), hydrating)vm._render返回了vnode,再传入vm._update中,由vm._update渲染成真实dom

    脚本宝典总结

    以上是脚本宝典为你收集整理的vue 源码学习笔记三 vue中如何生成虚拟DOM全部内容,希望文章能够帮你解决vue 源码学习笔记三 vue中如何生成虚拟DOM所遇到的问题。

    如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典推荐好友。

    本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
    如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。