
div idapp my-header :titlepageTitle/my-header my-content :postsposts/my-content my-footer/my-footer /divmy-header是什么HTML 标准里没有这个标签。但 Vue 能理解它把它变成一个完整的 DOM 子树。这就是组件的本质把一段模板、逻辑、样式打包成一个自定义标签像搭乐高一样搭页面。组件是什么从数据结构的角度看一个 Vue 组件就是一堆配置选项{ name: MyComponent, props: [title], data() { return { count: 0 } }, computed: { ... }, methods: { ... }, template: div{{ title }} - {{ count }}/div }当 Vue 遇到my-component它做的事情是拿出这个配置对象创建一个组件实例把实例挂载到 DOM 树中组件的注册全局注册Vue.component(my-component, { template: div我是一个全局组件/div })内部实现简化Vue.component function(id, definition) { // 把组件配置存到全局的 options.components 中 this.options.components[id] definition }全局组件在任何模板中都可以使用因为 Vue 会在找不到某个标签对应的原生 HTML 元素时去Vue.options.components中查找。局部注册new Vue({ components: { my-component: { template: div局部组件/div } } })局部组件只在当前实例及其子组件的模板中可用。组件实例的创建过程当 Vue 在模板中遇到一个组件标签时发生了什么模板: my-component :titlehello/my-component 第1步识别这是一个组件不是原生 HTML 标签 第2步找到组件的配置对象从全局/局部注册中 第3步创建组件实例 new VueComponent(options) 第4步执行组件的生命周期beforeCreate → created → beforeMount → mounted 第5步把组件的渲染结果VNode插入父组件的 VNode 树中创建组件实例的核心代码简化function createComponentInstance(vnode, parent) { const options { _isComponent: true, _parentVnode: vnode, // 组件在父模板中对应的 VNode parent: parent // 父组件实例 } // 调用 VueComponent 构造函数 return new vnode.componentOptions.Ctor(options) }组件通信父→子Props父组件传值给子组件child :nameparentName/childProps 的工作原理父组件的渲染函数中parentName是父组件 data 的一个响应式属性子组件的props声明了nameVue 在创建子组件时把父组件传递的值赋给子组件的_props.name子组件的_props也是响应式的——所以父组件的数据变了子组件也会自动更新简化实现function initProps(vm, propsOptions, propsData) { vm._props {} propsOptions.forEach(key { // 为每个 prop 创建响应式属性 defineReactive(vm._props, key, propsData[key]) // 代理到 vm 上让子组件可以用 this.name 访问 Object.defineProperty(vm, key, { get() { return vm._props[key] }, set() { console.warn(不要直接修改 prop) } }) }) }子→父$emit// 子组件 this.$emit(add, payload) // 父模板 child addhandleAdd/child$emit的原理非常直白Vue.prototype.$emit function(event, ...args) { // 找到父组件在当前组件的 VNode 上绑定的事件监听器 const listeners this._parentVnode.componentOptions.listeners if (listeners listeners[event]) { listeners[event](...args) // 直接调用 } }就是把父组件传进来的回调函数存起来$emit 的时候调用。跨层级Provide / Inject// 祖先 provide() { return { theme: dark } } // 后代不管隔了多少层 inject: [theme]实现原理简化function initProvide(vm) { // 把 provide 的结果合并到父组件的 _provided 上 vm._provided Object.create(vm.$parent ? vm.$parent._provided : null) const provide vm.$options.provide if (typeof provide function) { Object.assign(vm._provided, provide.call(vm)) } } function initInjections(vm) { // 沿原型链向上查找 const result resolveInject(vm.$options.inject, vm) Object.keys(result).forEach(key { defineReactive(vm, key, result[key]) }) } function resolveInject(inject, vm) { const result {} for (const key of inject) { let source vm while (source) { if (source._provided key in source._provided) { result[key] source._provided[key] break } source source.$parent // 向上查找 } } return result }利用了 JS 的原型链vm._provided的原型指向父组件的_provided向上查找自然遍历了整个组件树。插槽Slot插槽的本质是父组件写内容子组件用占位符接收。!-- 使用组件 -- my-layout h1 slotheader我的标题/h1 ← 父组件定义内容 p正文内容/p /my-layout !-- 组件定义 -- div classlayout slot nameheader/slot ← 子组件占位 slot/slot ← 默认插槽 /div插槽的 VNode 是在父组件的上下文中渲染的所以插槽内容访问的是父组件的数据。但插槽的挂载位置是在子组件内部。一个完整的组件生命周期new Vue({...})