vue2从数据变化到视图变化:props引起视图变化详解

396 阅读3分钟

prop是父子组件数据单向传递的桥梁,那么,父组件中数据变化后子组件中的视图是如何渲染的呢?

const childCom = {
  props: {
    childNum: {
      type: Number,
      default: function () {
        return 1
      }
    }
  },
  template: `<div>{{childNum}}</div>`
}

const parentCom = {
  components: {
    childCom
  },
  data() {
    return {
      parentData: 100,
    }
  },
  template: `<div @click="changeData">
    <childCom  :childNum = 'parentData'></childCom>
  </div>`,
  methods: {
    changeData() {
      this.parentData = 1000;
    }
  },
}
new Vue({
  el: "#app",
  render(h) {
    return h(parentCom);
  },
});

vue大体渲染逻辑是先通过this.init执行初始化操作,最后通过vm._update(vm._render(), hydrating)来执行渲染的逻辑,整个渲染过程又可以分为简单的模板渲染和复杂的组件渲染。
当前例子中,通过循环的方式从父级开始进行流程的执行。

1、render函数

这里通过compileToFunctions函数将模板<childCom @click.native="changeData" :childNum = 'parentData'></childCom>转换成:

function anonymous() {
  with (this) {
    return _c("childCom", {
      attrs: { childNum: parentData },
      nativeOn: {
        click: function ($event) {
          return;
          changeData($event);
        },
      },
    });
  }
}

这里的属性attrs: { childNum: parentData }其中的parentDatawith(this)作用域下就是this.parentData,即attrs: { childNum: 100 }

2、createComponent函数

createComponent创建组件vNode的过程中,有var propsData = extractPropsFromVNodeData(data, Ctor, tag):

function extractPropsFromVNodeData (
  data,
  Ctor,
  tag
) {
  // we are only extracting raw values here.
  // validation and default values are handled in the child
  // component itself.
  var propOptions = Ctor.options.props;
  if (isUndef(propOptions)) {
    return
  }
  var res = {};
  var attrs = data.attrs;
  var props = data.props;
  if (isDef(attrs) || isDef(props)) {
    for (var key in propOptions) {
      var altKey = hyphenate(key);
      if (process.env.NODE_ENV !== 'production') {
        var keyInLowerCase = key.toLowerCase();
        if (
          key !== keyInLowerCase &&
          attrs && hasOwn(attrs, keyInLowerCase)
        ) {
          tip(
            "Prop \"" + keyInLowerCase + "\" is passed to component " +
            (formatComponentName(tag || Ctor)) + ", but the declared prop name is" +
            " \"" + key + "\". " +
            "Note that HTML attributes are case-insensitive and camelCased " +
            "props need to use their kebab-case equivalents when using in-DOM " +
            "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"."
          );
        }
      }
      checkProp(res, props, key, altKey, true) ||
      checkProp(res, attrs, key, altKey, false);
    }
  }
  return res
}
function checkProp (
  res: Object,
  hash: ?Object,
  key: string,
  altKey: string,
  preserve: boolean
): boolean {
  if (isDef(hash)) {
    if (hasOwn(hash, key)) {
      res[key] = hash[key]
      if (!preserve) {
        delete hash[key]
      }
      return true
    } else if (hasOwn(hash, altKey)) {
      res[key] = hash[altKey]
      if (!preserve) {
        delete hash[altKey]
      }
      return true
    }
  }
  return false
}

这里通过var propOptions = Ctor.options.props获取到propOptionschildNum: { type: Number, default: function () { return 1 } },执行checkProp(res, props, key, altKey, true)返回false,继续执行到 checkProp(res, attrs, key, altKey, false)的方式,将属性attrs: { childNum: 100 }key作为参数传入执行获取到{childNum: 100}。最后将propsData做为传入创建vNode实例:

const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

3、patch函数

在获取到vNode后会执行patchcreateElm中,这里是组件vNode,进而执行到逻辑createComponent

if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  return
}
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false /* hydrating */)
      }
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

执行到i(vnode, false /* hydrating */),这里的i是钩子函数init

// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
  // ...
}

实例化const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance )的过程中会执行到构造函数的this._init

4、initProps

this._init初始化过程中执行到initState(vm)

function initState (vm) {
  // ...
  var opts = vm.$options;
  if (opts.props) { initProps(vm, opts.props); }
  // ...
}
function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

这里对defineReactive(props, key, value)进行响应式处理,当进行vNode创建过程中会访问到props中的childNum,通过dep.depend();进行依赖收集,此时childNum锁定的发布者depsubs就有用来渲染子组件childCom的渲染watcher
响应式处理请移步juejin.cn/post/713099…

5、$mount函数

当执行完const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance )后,执行child.$mount(hydrating ? vnode.elm : undefined, hydrating)进行child的挂载。最后得到child.$el"<div>100</div>"
组件渲染请移步juejin.cn/post/712909…

6、数据改变时的逻辑

(1)dep.notify

当数据发生改变时,this.parentData发生变化,执行发布者depdep.notify进行父组件的重新渲染。

(2)patch

父组件patch的过程中会执行到patchVnode

if (!isRealElement && sameVnode(oldVnode, vnode)) {
    // patch existing root node
    patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
  }
function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    // ...

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }
    // ...
  }

执行到i(oldVnode, vnode),这里的i是钩子函数prepatch

// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
  // ...
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },
  // ...
}

主要看updateChildComponent中关于props的更新:

export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  // ...
  // update props
  if (propsData && vm.$options.props) {
    toggleObserving(false)
    const props = vm._props
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions: any = vm.$options.props // wtf flow?
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    toggleObserving(true)
    // keep a copy of raw propsData
    vm.$options.propsData = propsData
  }
  // ...
}

在执行props[key] = validateProp(key, propOptions, propsData, vm)的过程中,对props[key]进行了计算并赋值,进而引起props[key]锁定的发布者depdep.notify的执行,进而执行子组件的重新渲染。

以上就是prop改变引起子组件的全过程。

小结:

prop改变的过程中主要关注renderwith(this)中的prop真实值、propsData的获取、initProps过程中对prop进行响应式处理以及数据变化时prepatch的执行。