Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue 2.0 父子组件通讯 #17

Open
CommanderXL opened this issue Jan 4, 2019 · 0 comments
Open

Vue 2.0 父子组件通讯 #17

CommanderXL opened this issue Jan 4, 2019 · 0 comments
Labels
Vue.js Extra attention is needed

Comments

@CommanderXL
Copy link
Owner

父子组件通讯

Vue中,父子组件基本的通讯方式就是父组件通过props属性将数据传递给子组件,这种数据的流向是单向的,当父props属性发生了改变,子组件所接收到的对应的属性值也会发生改变,但是反过来却不是这样的。子组件通过event自定义事件的触发来通知父组件自身内部所发生的变化。

Vue props 是如何传递以及父 props 更新如何使得子模板视图更新

还是从一个实例出发:

// 模板
<div id="app">
  <child-component :message="val"></child-component>
</div>


// js

Vue.component('child-component', {
  props: ['message']
  template: '<div>this is child component, I have {{message}}</div>',
})

new Vue({
  el: '#app',
  data() {
    return {
      val: 'parent val'
    }
  },
  mounted () {
    setTimeout(() => {
      this.val = 'parent val which has been changed after 2s'
    }, 2000)
  }
})

最终页面渲染出的内容为:

this is child component, I have parent val

2s后文案变更为:
this child component, I hava parent val which has been changed after 2s

接下来我们就来看下父子组件是如何通过props属性来完成数据的传递的。

首先根组件开始实例化,完成一系列的初始化的内容。首先将val转化为响应式的数据,并调用Vue.prototype.$mount方法完成vnode的生成,真实dom元素的挂载等功能:

Vue.prototype._init = function (options) {
  ...
  initState(vm)

  ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el);
  }
  ...
}

Vue.prototype.$mount方法内部:

Vue.prototype.$mount = function (
  el,
  hydrating
) {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating)
};

var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
  el,
  hydrating
) {
  el = el && query(el);

  var options = this.$options;
  // resolve template/el and convert to render function
  if (!options.render) {
    var template = options.template;
    if (template) {
      ...
    } else if (el) {
      template = getOuterHTML(el);
    }
    if (template) {
      ...
      var ref = compileToFunctions(template, {
        shouldDecodeNewlines: shouldDecodeNewlines,
        shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this);
      // 生成render函数
      var render = ref.render;
      var staticRenderFns = ref.staticRenderFns;
      options.render = render;
      options.staticRenderFns = staticRenderFns;
    }
  }
  return mount.call(this, el, hydrating)
};

function mountComponent (
  vm,
  el,
  hydrating
) {
  vm.$el = el;
  if (!vm.$options.render) {
    ...
  }
  // 挂载前
  callHook(vm, 'beforeMount');

  var updateComponent;
  /* istanbul ignore if */
  if ("development" !== 'production' && config.performance && mark) {
    ...
  } else {
    updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };
  }

  new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */);
  hydrating = false;

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true;
    callHook(vm, 'mounted');
  }
  return vm
}

完成模板的编译,同时生成render函数,这个render函数在实际实行生成vnode时,会将作用域绑定到对应的vm实例的作用域下,即在创建vnode的环节当中,始终访问的是当前这个vm实例,子vnode创建时是没法直接访问到父组件中定义的数据的。除非通过props属性来完成数据由父组件向子组件的传递。

;(function() {
  with (this) {
    return _c(
      'div',
      {
        attrs: {
          id: 'app'
        }
      },
      [
        _c('my-component', {
          attrs: {
            message: val
          }
        })
      ],
      1
    )
  }
})

完成模板的编译生成render函数后,调用_c方法,对应访问vue实例的_c方法,开始创建对应的vnode,注意这里val变量,即vue实例上data属性定义的val,在创建对应的vnode前,实例已经调用initState方法将val转化为响应式的数据。因此在创建vnode过程中,访问val即访问它的getter

在访问过程中Dep.target已经被设定为当前vue实例的watcher(具体见mountComponent方法内部创建watcher对象),因此会将当前的watcher加入到valdep当中。这样便完成了val的依赖收集的工作。

在创建VNode时,又分为:

  • 内置标签(即标准规定的标签)元素的 VNode(built in VNode)
  • 本文要讨论的自定义的标签元素的 VNode(component VNode)

其中内置标签的VNode的没有需要特别说明的地方,就是调用VNode的构造函数完成创建过程。

但是在创建自定义标签元素的VNode时,完成一些重要的操作(因为本文是讲解 props 传递,所以挑出和 props 相关的部分):

function createComponent () {

  ...
  // 注意这个方法。它完成了从父组件对应的props字段获取值的作用,具体到本例子,就是获取到了message字段的值
  // 这样就完成了props从父组件传递到子组件的功能
  var propsData = extractPropsFromVNodeData(data, Ctor, tag);
  ...

  // 给component初始化挂载钩子函数,在VNode实例化成vue component会调相关的钩子函数
  installComponentHooks(data);
  ...
  // 创建VNode
  var vnode = new VNode(
    ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
    data, undefined, undefined, undefined, context,
    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
    asyncFactory
  );
  return vnode
}

createComponent创建VNode的过程中需要注意的是:Vue的父子组件传递props属性的时候都是在子组件上直接写自定义的dom attrs

<div id="app">
  <child-component :message="val"></child-component>
</div>

但是在模板编译后,统一将dom节点(不管是built in的节点还是自定义component节点)上的属性转化为attrs对象(见代码片段 111),在创建VNode过程中调用了extractPropsFromVNodeData这个方法完成从attrs对象上获取到这个component所需要的props属性,获取完成后还会将attrs对象上对应的key值删除。因此这个key值对应的是要传入子component的数据,而非原生dom属性,最终由VNode生成真实dom的时候是不需要这些自定义数据的,因此需要删除。

当然如果你在子组件中传入了props数据,但是在子组件中没有定义相关的props属性,那么这个 props 属性最终会渲染到子组件的真实的dom元素上,不过控制台也会出现报错:

> [Vue warn]: Property or method "message" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property

当完成了my-componentVNode创建后,开始创建它的父VNode,即根VNode

(function() {
  with (this) {
    return _c('div', {
        attrs: {
          "id": "app"
        }
      }, <my-component-vnode>, 1)
   }
  }
)

vm._render()方法调用完成后,即所有的VNode都创建完成,开始递归将VNode渲染成真实的dom节点,同时挂载到document当中(见上方调用的mountComponent内部vm.update(vm._render()))。

Vue.prototype._update = function (vnode, hydrating) {
    var vm = this;
    if (vm._isMounted) {
      // 触发beforeUpdate钩子函数
      callHook(vm, 'beforeUpdate');
    }
    var prevEl = vm.$el;
    var prevVnode = vm._vnode;
    var prevActiveInstance = activeInstance;
    activeInstance = vm;
    vm._vnode = vnode;
    // 第一次渲染
    if (!prevVnode) {
      // initial render
      // 初始化render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      );
      ...
    } else {
      // updates
      // 将prevVnode和vnode进行patch操作并更新
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
    ...
  }

在将VNode递归渲染成真实的dom节点过程当中:

对于**自定义标签元素(即组件)**的渲染,首先完成组件vue实例的初始化。又重复到上文一开始的Vue.prototype._init方法。在实例化my-component组件的过程中,还是通过调用initState方法,将定义的props属性中的message属性转化为响应式的数据。� 在此之前,my-component组件上的message属性已经被初始化为从父组件传递过来的值。因此在页面初次渲染的时候,my-component通过定义的props属性从父组件上获取到的值为parent val(上面的例子中定义的)

这样便完成了父组件通过props属性向子组件传递数据。

父 props 的改变是如何影响到子 component 的视图的更新

在子组件生成 VNode 的过程中会对应创建 render watcher,通过 props 从父组件传递给子组件的数据是在父作用域下获取得到的。因此,props 的 Dep 中会将这个 watcher 作为依赖添加进去。那么当父组件中的数据发生了改变,便会调用这个响应式数据Dep.notify()方法去通知相关的订阅者去完成更新,其中就包括子组件的 render watcher。

Vue 父子组件如何传递/绑定自定义事件的

那么在子组件需要和父组件进行通讯的时候,所使用的events事件又是如何实现的呢?

// 模板
<div id="app">
  <child-component @foo="bar"></child-component>
</div>


// js
Vue.component('child-component', {
  props: ['message']
  template: '<div @click="foo">this is child component, I have {{message}}</div>',
  methods: {
    foo () {
      this.$emit('foo', 'this is child component')
    }
  }
})

new Vue({
  el: '#app',
  data() {
    return {
      val: 'parent val'
    }
  },
  methods: {
    foo (val) {
      console.log(val)
    }
  }
})

当点击<child-component>时,会在控制台输出this is child component。那我们来看下整个过程是如何进行的:

首先在模板编译的过程:

;(function() {
  with (this) {
    return _c(
      'div',
      {
        attrs: {
          id: 'app'
        }
      },
      [
        _c('my-component', {
          staticClass: 'my-component',
          attrs: {
            message: val
          },
          on: {
            test: test
          }
        })
      ],
      1
    )
  }
})

在创建my-componentcomponent VNode过程中,通过传入data数据上定义的on属性。这个时候test访问的还是在父组件上定义的test方法。

//
function createComponent (
  Ctor,
  data,
  context,
  children,
  tag
) {
  ...
  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  // 获取父 component 作用域中定义的 listeners。这个 listeners 会作为 componentOptions 的属性传递进 VNode 当中
  // 注意这个地方和 DOM listeners 的区别。DOM listeners是使用的浏览器原生的事件系统
  var listeners = data.on;
  ...
  // install component management hooks onto the placeholder node
  installComponentHooks(data);

  // return a placeholder vnode
  var name = Ctor.options.name || tag;
  // 将从父 component 作用作用域定义的 listeners 作为 VNode 的 componentOptions 传入 VNode 的构造函数内部
  var vnode = new VNode(
    ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
    data, undefined, undefined, undefined, context,
    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
    asyncFactory
  );

  /* istanbul ignore if */
  return vnode
}

接下来在将这个VNode实例成vue component的时候:

  Vue.prototype._init = function (options) {
    var vm = this;

    // 实例化子vue component
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      // 从VNode的componentOptions属性上获取关于这个vue component定义的属性
      initInternalComponent(vm, options);
    } else {
      ...
    }
    // expose real self
    vm._self = vm;
    initLifecycle(vm);
    // 初始化vm的上绑定的自定义事件
    initEvents(vm);
    ...
  };

  // 实例化vue component自定义事件
  function initEvents (vm) {
    vm._events = Object.create(null);
    vm._hasHookEvent = false;
    // init parent attached events
    // 获取从父组件上传递过来的自定义事件
    var listeners = vm.$options._parentListeners;
    if (listeners) {
      updateComponentListeners(vm, listeners);
    }
  }

  function updateComponentListeners (
    vm,
    listeners,
    oldListeners
  ) {
    // 设置全局target对象
    target = vm;
    updateListeners(listeners, oldListeners || {}, add, remove$1, vm);
    target = undefined;
  }

  // 给全局target对象挂载自定义事件
  function add (event, fn, once) {
    if (once) {
      target.$once(event, fn);
    } else {
      target.$on(event, fn);
    }
  }

在将VNode实例化过程当中,调用initEvents方法,获取在这个VNode上绑定的从父组件传递下来的方法,并缓存至对应事件的回调函数数组当中。当你在子组件当中去$emit对应的事件的时候,便会执行对应的回调函数。这里父子间的event事件机制实际上是利用了发布订阅的设计模式。

这个是有关父子组件自定义事件的机制。这里也顺带讲下 Vue 是如何绑定原生 DOM 事件的。

在代码片段 xxx 当中,生成 VNode 的环节当中,会将 nativeOn 赋值给data.on(data 上保存了将 VNode 渲染成真实 DOM 节点的数据)。当开始渲染真实 DOM 元素的时候:

function createElm () {
  ...
  if (isDef(data)) {
    invokeCreateHooks(vnode, insertedVnodeQueue)
  }
  ...
}

function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
    cbs.create[i$1](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);
    }
  }
}

当 data 有值的时候,那么就开始执行 DOM 相关属性更新的工作。即执行在 cbs 上有关 create 阶段所有的回调函数,其中包括:

var platformModules = [
  attrs, // attrs 属性
  klass, // class 
  events, // 原生 dom 事件
  domProps,
  style,
  transition
]

其中我们来看下有关 events,即原生 dom 事件是如何绑定到 DOM 元素上的。

var events = {
  create: updateDOMListeners,
  update: updateDOMListeners
}

function updateDOMListeners (oldVnode, vnode) {
  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
    return
  }
  var on = vnode.data.on || {};
  var oldOn = oldVnode.data.on || {};
  target$1 = vnode.elm; // 真实的 dom 节点
  normalizeEvents(on);
  updateListeners(on, oldOn, add$1, remove$2, vnode.context);
  target$1 = undefined;
}

function add$1 (
  event,
  handler,
  once$$1,
  capture,
  passive
) {
  handler = withMacroTask(handler); // 强制放到 marcoTask 当中去执行
  if (once$$1) { handler = createOnceHandler(handler, event, capture); }
  target$1.addEventListener(
    event,
    handler,
    supportsPassive
      ? { capture: capture, passive: passive }
      : capture
  );
}

function remove$2 (
  event,
  handler,
  capture,
  _target
) {
  (_target || target$1).removeEventListener(
    event,
    handler._withTask || handler,
    capture
  );
}

在初次渲染DOM节点的时候,传入的 oldVNode 为一个空的 VNode,即拿这个空的 VNode 和即将要渲染的 VNode 进行原生DOM事件的 diff 工作。在updateDOMListeners方法当中还是继续调用updateListeners方法去进行事件的绑定,这个时候绑定事件的函数使用的是add$1,即调用DOM提供的addEventListener方法去完成原生DOM事件的绑定工作。在这里我们也可以看出去 Vue 提供的事件修饰符在这里进行配置生效。这样便完成了原生的DOM事件的绑定。

.sync修饰符-数据双向绑定

在 2.3.0+ 版本,Vue 提供了一种可以对 props 进行数据双向绑定的语法糖。基本的使用方法为:

事实上是 Vue 在将模板编译成渲染函数时,会将带有.sync标识符的 props 自动添加一个自定义的事件update:message事件

{
  ...
  on: {
    'update:message': function ($event) {
      message = $event
    }
  }
  ...
}

那么当你在子组件当中去调用update:message方法的时候,并传入值的时候即会更新 message 的值。这个 message 的值即在父组件当中的数据。这样便完成了数据的双向绑定。

// vm._update(vm._render())
// patch
// prepatch 方法
// updateChildComponent 完成 props 等属性的 setter 操作 _props是在实例初始化过程中定义的一个内部属性,同时调用defineReactive方法完成将响应式数据存放到_props属性上。在VNodepatch过程中,如果有属性发生了变化,那么会调用这个属性的setter方法完成值的变更操作,继而完成视图的更新。当然了,如果组件在定义的过程,没有定义props属性,那么在实例初始化的过程中,_props属性也不会被创建。只有组件上定义过props属性,在初始化的过程中才会定义这个内部属性。

@CommanderXL CommanderXL added the Vue.js Extra attention is needed label Jan 6, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Vue.js Extra attention is needed
Projects
None yet
Development

No branches or pull requests

1 participant