# vue进阶 **Repository Path**: dawnYL/vue-advanced ## Basic Information - **Project Name**: vue进阶 - **Description**: vue 进阶学习。。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-06-04 - **Last Updated**: 2022-08-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Vue ## 组件化实践 ### 基础 #### Vue render 函数 Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的 完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。 render 函数接收一个 createElement 函数来创建虚拟 Dom 并返回这个虚拟 Dom VDom ```js render: function (createElement) { // createElement函数返回结果是VNode return createElement( tag, // 标签名称 data, // 传递数据 children // 子节点数组 ) } ``` #### Vue plugin 插件 Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一 个可选的选项对象: ```js MyPlugin.install = function (Vue, options) { // 1. 添加全局方法或属性 Vue.myGlobalMethod = function () {}; // 2. 添加全局资源 Vue.directive('my-directive', {}); // 3. 注入组件选项 Vue.mixin({ created: function () { // 逻辑... }, }); // 4. 添加实例方法 Vue.prototype.$myMethod = function (methodOptions) {}; }; // Vue.use(MyPlugin, { someOption: true }) ``` #### VueRouter addRouters 函数 ```js // https://router.vuejs.org/zh/api/#router-addroutes router.addRoutes(routes: Array) ``` #### VueX 模块化 ```js const store = new Vuex.Store({ modules: { user: { namespaced: true, state: { isLogin: false, }, mutations: { setIsLogin(state, login) { state.isLogin = login; }, }, }, }, }); // this.$store.state.user.isLogin // this.$store.state.user.commit('setIsLogin',true) // ...mapState(['user/isLogin']) // ...mapmutation(['user/setIsLogin']) // 改进写法 // ...mapState('user',['isLogin']) // ...mapmutation('user',['setIsLogin']) ``` #### VueX plugin 插件 使用 ```js improt myPlugin from './plugins/persist.js'; const store = new Vuex.Store({ // ... plugins: [myPlugin] }) ``` ./plugins/persist.js ```js export default (store) => { // 初始化时从localStorage获取数据 if (localStorage) { const user = JSON.parse(localStorage.getItem('user')); if (user) { store.commit('user/login'); store.commit('user/setUsername', user.username); } } // 用户状态发生变化时缓存之 store.subscribe((mutation, state) => { if (mutation.type.startsWith('user/')) { localStorage.setItem('user', JSON.stringify(state.user)); } else if (mutation.type === 'user/logout') { localStorage.removeItem('user'); } }); }; ``` ### 组件之间的通信 #### props ⽗给⼦传值 ```js // child props: { msg: String; } // parent ; ``` #### $emit/$on ⼦给⽗传值 ```js // child this.$emit('add', good) // parent ``` #### 事件总线 ```js // Bus:事件派发、监听和回调管理 class Bus { constructor() { this.callbacks = {}; } $on(name, fn) { this.callbacks[name] = this.callbacks[name] || []; this.callbacks[name].push(fn); } $emit(name, args) { if (this.callbacks[name]) { this.callbacks[name].forEach((cb) => cb(args)); } } } // main.js Vue.prototype.$bus = new Bus(); // child1 this.$bus.$on('foo', handle); // child2 this.$bus.$emit('foo'); // 最常用 main.js Vue.prototype.$bus = new Vue(); ``` > 实践中通常⽤ Vue 代替 Bus,因为 Vue 已经实现了相应接⼝ #### vuex 创建唯⼀的全局数据管理者 store,通过它管理数据并通知组件状态变更。 #### $parent/$root 兄弟组件之间通信可通过共同祖辈搭桥,$parent或$root。 ```js // brother1 this.$parent.$on('foo', handle); // brother2 this.$parent.$emit('foo'); ``` #### $children ⽗组件可以通过$children 访问⼦组件实现⽗⼦通信。 ```js // parent this.$children[0].xx = 'xxx'; ``` > 注意:$children 不能保证⼦元素顺序,同步组件情况下,下标就是子组件在父组件中出现的顺序,但是可能有异步组件(包括 v-if),顺序就会出错。主要是看子组件的挂载时间。 > > 问:和$refs 有什么区别? > > 答:$children获取到的是父组件中自定义组件数组,$refs 还可以获取到一些普通的 HTML 标签 #### $attrs/$listeners - $attrs 包含了⽗作⽤域中**不作为 prop 被识别** (且获取) 的特性绑定 ( class 和 style 除外)。当⼀个组件没有声明任何 prop 时,这⾥会包含所有⽗作⽤域的绑定 ( class 和 style 除外),并且可以通过 vbind="$attrs" 传⼊内部组件——在创建⾼级别的组件时⾮常有⽤。 ``` // child:并未在props中声明foo,在props中声明了的这里没有

{{$attrs.foo}}

// parent // ------------ // 爷爷组件调用父亲组件 // 父亲组件给 孙子组件传值 v-bind 会解析对象进行属性展开 // 孙子组件

{{ $attrs.msg }}

methods: { test() { // 通过 $listeners 传递进来的 触发的是爷爷组件的事件 this.$emit('foo'); }, }, ``` > [文档](https://cn.vuejs.org/v2/api/#vm-attrs) > > props 中声明了 在$attrs 中就获取不到 - $listeners 包含了父作用域中的 (不含 `.native` 修饰器的) `v-on` 事件监听器。它可以通过 `v-on="$listeners"` 传入内部组件——在创建更高层次的组件时非常有用。 #### provide/inject 能够实现祖先和后代之间传值---多层级推荐使用 ```js // ancestor provide() { return {foo: 'foo',name: value} } // descendant inject: ['foo','name'] ``` #### refs 获取⼦节点引⽤ ```js // parent mounted() { this.$refs.hw.xx = 'xxx' } ``` ### 插槽 插槽语法是 Vue 实现的内容分发 API,⽤于复合组件开发。该技术在通⽤组件库开发中有⼤量应⽤。 #### 匿名插槽 ```js // comp1
// parent hello ``` #### 具名插槽 将内容分发到⼦组件指定位置 ```js // comp2
// parent ``` #### 作⽤域插槽 ```js // comp3
// parent ``` ### 组件实战 表单 yl-form - 指定数据,校验规则 获取到所有需要校验的子元素,做一个全局校验的方法 > 全局校验需要获取到所有的 formItem 进行进行校验 > > 使用找子元素的方法可能消耗性能太大,就使用了一个数组装子元素,子元素 mounted 的时候告诉父元素我来了,父元素 created 的时候监听,遇到子元素就装到数组里面,校验的时候遍历这个数组 yl-form-item - label - 校验 prop 有这个属性的时候就校验 没有就不校验 - 错误提示 > 自己单个校验需要获取到 form 上定义的规则 inject/ prop yl-Input - 维护数据 > 需要通知父组件(formItem)进行校验 $parent 可以实现 但是耦合高 #### yl-form 实现 ```vue ``` #### yl-formItem 实现 ```vue ``` #### yl-Input 实现 ```vue ``` #### 使用 ```html ``` ### 组件实战 弹窗 #### Notice 组件 ```vue ``` #### create 方法 ```js import Vue from 'vue'; /** 需求 * 传入一个组件和组件的配置 * 创建组件的实例,并且将它挂载到body上 * 返回这个组件实例 */ export default function create(Component, props) { // 实例创建 // 作业:使用extend方式创建组件实例并挂载 // extend方法返回的组件构造函数 // const Ctor = Vue.extend(Component) // 获取组件实例 是一个虚拟dom // const comp = new Ctor({propsData:props}) // comp.$mount(); // document.body.appendChild(comp.$el); // 删除函数 给组件绑定一个方法 // comp.remove = () => { // // 移除dom // document.body.removeChild(comp.$el); // 销毁组件; // comp.$destroy(); // }; // 方式二:借鸡生蛋 参考main.js 中的写法 const vm = new Vue({ render: (h) => h(Component, { props }), // $mount()本质上将vdom=>dom 不能直接挂载到 body上,$mount()是一个覆盖过程 }).$mount(); // 通过 vm.$el 获取生成的 真实dom,绑定到 body上 document.body.appendChild(vm.$el); // 获取组件实例 --> component const comp = vm.$children[0]; // 删除函数 给组件绑定一个方法 comp.remove = () => { // 移除dom document.body.removeChild(vm.$el); 销毁组件; vm.$destroy(); }; // 返回组件实例 return comp; } ``` #### 使用 ```js // 创建notice实例 create(Notice, { title: '村长喊你来搬砖', message: isValid ? '请求登录' : '校验失败', duration: 3000, }).show(); ``` #### 插件化 ```js import Notice from '../components/Notice.vue'; import create from '../utils/create'; export default { install(Vue) { Vue.prototype.$notice = function (options) { const comp = create(Notice, options); comp.show(); return comp; }; }, }; ``` ### 组件化思路 ![组件化思路](http://typora.coderyl.top/typora/%E7%BB%84%E4%BB%B6%E5%8C%96%E6%80%9D%E8%B7%AF.png) > https://www.processon.com/view/link/5d430271e4b01ed2c6aa4171#map ## 全家桶原理分析 ### VueRouter #### 核心步骤 - 步骤⼀:使⽤ vue-router 插件,router.js ```js import Router from 'vue-router'; Vue.use(Router); ``` - 步骤⼆:创建 Router 实例,router.js ```js export default new Router({...}) ``` - 步骤三:在根组件上添加该实例,main.js ```js import router from './router'; new Vue({ router, }).$mount('#app'); ``` - 步骤四:添加路由视图,App.vue ```html ``` - 导航 ```html Home About ``` #### 提出疑问 - router-link router-view 标签哪里来的? - spa 单页面应用程序不能刷新页面 - hash #xxx - history api - 监听事件 hashchange 通知 router-view 更新 - 利用 vue 的数据响应式,制造一个响应式的数据表示当前 url,url 改变了 router-view 组件重新调用 render 函数 - Vue.use(Router) 和 mian.js 中 new Vue 的时候传入的 router 内部到底干了什么? - this.$router this.$route 哪来的 - 在插件的 install 方法中 获取到的 new Vue 的配置 绑定到了原型链上 #### 起步 - 实现一个插件 - 实现 VueRouter 类 - 实现 install 方法 - 挂载 $router - install 中 将 new Vue 传入的 router 对象绑定到 Vue 原型上,不过因为有执行时间循序关系,需要用一个 mixin 混入 - 实现两个全局的组件 - 一个渲染 跳转链接,一个渲染跳转的视图 - 构建一个类 在 new 的时候传入路由配置 - 能监听路由的变化 - 构建一个响应式的数据 YLVue.util.defineReactive - 数据变导致 router-view 改变,router-view 中的数据是在 new 的时候配置的对象中获取到的 #### 核心代码 ```js let YLVue; // 实现一个 路由插件 class YLVueRouter { constructor(options) { this.$options = options; // 缓存path和route映射关系 this.routeMap = {}; this.$options.routes.forEach((route) => { this.routeMap[route.path] = route; }); // 数据响应式 // this.current = '/'; const initial = window.location.hash.slice(1) || '/'; // $set不行它的对象不能是 Vue 实例,或者 Vue 实例的根数据对象。 YLVue.util.defineReactive(this, 'current', initial); window.addEventListener('hashchange', this.onHashChange.bind(this)); window.addEventListener('load', this.onHashChange.bind(this)); } onHashChange() { // #/about this.current = window.location.hash.slice(1); } } // 插件需要一个静态的install方法,参数是Vue的构造函数 YLVueRouter.install = function (Vue) { // 保持构造函数 YLVue = Vue; // 挂载 $router // 问题:install方法是 use的时候执行 在执行这里的时候 实例对象还没有被创建 Vue.mixin({ beforeCreate() { // 做一个全局的混入,组件实例化的时候才执行,这个时候 router实例已经被创建了 // this是指组件实例 // $options 是在 main.js 中 new Vue传入的对象 if (this.$options.router) { // 避免 $router 被多次的挂载到原型上 Vue.prototype.$router = this.$options.router; } }, }); // 实现两个全局组件 Vue.component('router-link', { props: { to: { type: String, required: true }, }, render(h) { // 支持jsx语法 需要一个 loader库 // return {this.$slots.default}; return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default); }, }); Vue.component('router-view', { render(h) { // 获取 路由表 根据url 匹配路由 // const routes = this.$router.$options.routes; // const current = this.$router.current; // const route = routes.find((route) => route.path === current); // const comp = route ? route.component : null; const { routeMap, current } = this.$router; const comp = routeMap[current] ? routeMap[current].component : null; return h(comp); }, }); }; export default YLVueRouter; ``` #### 使用 - 步骤⼀:使⽤ vue-router 插件,router.js ```js import Router from './ylvue-router'; Vue.use(Router); ``` - 步骤⼆:创建 Router 实例,router.js ```js export default new Router({...}) ``` - 步骤三:在根组件上添加该实例,main.js ```js import router from './router'; new Vue({ router, }).$mount('#app'); ``` ### VueX > Vuex **集中式**存储管理应⽤的所有组件的状态,并以相应的规则保证状态以**可预测**的⽅式发⽣变化。 #### 核心步骤 ```js import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ ...options }) import store from './store' new Vue({ store, }).$mount("#app");

counter: {{ $store.state.counter }}

``` #### 起步 - 实现⼀个插件:声明 Store 类,挂载$store - Store 具体实现: - 创建响应式的 state,保存 mutations、actions 和 getters - 实现 commit 根据⽤户传⼊ type 执⾏对应 mutation - 实现 dispatch 根据⽤户传⼊ type 执⾏对应 action,同时传递上下⽂ - 实现 getters,按照 getters 定义对 state 做派⽣ - 核心代码 ```js let ylVue; // 实现store 类 class Store { constructor(options) { this.$options = options; // 将 this 绑定到 store实例 const store = this; const { commit, dispatch } = store; this.commit = function boundCommit(type, payload) { commit.call(store, type, payload); }; this.dispatch = function boundDispatch(type, payload) { dispatch.call(store, type, payload); }; // 遍历 getters所有的 key ,动态的赋值,值是函数的执行结果 // 确保是 响应式的 // 缓存一下 this._wrappedGetters = options.getters; // 定义 computed const computed = {}; this.getters = {}; Object.keys(this._wrappedGetters).forEach((key) => { const fn = store._wrappedGetters[key]; // computed 的函数没有参数 就做了一个高阶函数 computed[key] = function () { return fn(store.state); }; // 设置只读属性 Object.defineProperty(store.getters, key, { get() { return store._vm[key]; }, }); }); // 构建响应式的数据 this._vm = new ylVue({ data: { $$state: options.state, }, computed, }); // 保存 mutations this._mutations = options.mutations || {}; // 保存 actions this._actions = options.actions || {}; } commit(type, payload) { // 根据type 获取 mutation let entry = this._mutations[type]; if (!entry) { console.error('一个 未知的 mutation type'); return; } entry(this.state, payload); } dispatch(type, payload) { // 根据type 获取 mutation let entry = this._actions[type]; if (!entry) { console.error('一个 未知的 action type'); return; } return entry(this, payload); } // 做成一个私有的 不让用户去覆盖 get state() { return this._vm._data.$$state; } set state(v) { console.error('please use replaceState to reset state'); } } // 实现 install 方法 function install(Vue) { ylVue = Vue; Vue.mixin({ beforeCreate() { if (this.$options.store) { Vue.prototype.$store = this.$options.store; } }, }); } export default { Store, install }; ``` #### 组件化思路 ![image-20210524192514688](http://typora.coderyl.top/typora/image-20210524192514688.png) > http://typora.coderyl.top/typora/image-20210524192514688.png ## 手写 Vue1 ### 设计思想 MVVM 模式 ![image-20210524193122047](http://typora.coderyl.top/typora/image-20210524193122047.png) MVVM 框架的三要素:**数据响应式、模板引擎及其渲染** 数据响应式:监听数据变化并在视图中更新 - Object.defineProperty() - Proxy 模版引擎:提供描述视图的模版语法 - 插值:{{}} - 指令:v-bind,v-on,v-model,v-for,v-if 渲染:如何将模板转换为 html - 模板 => vdom => dom ### 数据响应式原理 数据变更能够响应在视图中,就是数据响应式。vue2 中利⽤ Object.defineProperty() 实现变更检测。 ```js // 数据响应式 // 对象的 key设置成响应式 默认值是 val function defineReactive(obj, key, val) { // 递归处理 observe(val); Object.defineProperty(obj, key, { get() { console.log('get', key); return val; }, set(newVal) { if (newVal !== val) { console.log('set', key, newVal); observe(newVal); val = newVal; } }, }); } // 让我们使一个对象所有属性都被拦截 function observe(obj) { if (typeof obj !== 'object' || obj == null) { return; } Object.keys(obj).forEach((key) => { defineReactive(obj, key, obj[key]); }); } // 由于新增属性无法被拦截,所以必须有特定api做对应响应式拦截 function set(obj, key, val) { defineReactive(obj, key, val); } const obj = { foo: 'foo', bar: 'bar', baz: { a: 1 } }; // defineReactive(obj, 'foo', 'foo') observe(obj); // obj.foo // obj.foo = 'fooooooooooo' // obj.bar // obj.baz.a // obj.baz = {a: 100} // obj.baz.a // obj.dong = 'dong' set(obj, 'dong', 'dong'); obj.dong; ``` ### Vue 中的数据响应式 #### 原理分析 1. new Vue() ⾸先执⾏初始化,对 data 执⾏响应化处理,这个过程发⽣在 Observer 中 2. 同时对模板执⾏编译,找到其中动态绑定的数据,从 data 中获取并初始化视图,这个过程发⽣在 Compile 中 3. 同时定义⼀个更新函数和 Watcher,将来对应数据变化时 Watcher 会调⽤更新函数 4. 由于 data 的某个 key 在⼀个视图中可能出现多次,所以每个 key 都需要⼀个管家 Dep 来管理多个 Watcher 5. 将来 data 中数据⼀旦发⽣变化,会⾸先找到对应的 Dep,通知所有 Watcher 执⾏更新函数 ![image-20210524213927272](http://typora.coderyl.top/typora/image-20210524213927272.png) #### 涉及类型介绍 KVue:框架构造函数 Observer:执⾏数据响应化(分辨数据是对象还是数组) Compile:编译模板,初始化视图,收集依赖(更新函数、watcher 创建) Watcher:执⾏更新函数(更新 dom) Dep:管理多个 Watcher,批量更新 #### New YLVue 需要做什么 - 首先就是将自己的配置保存一份(`$options`), - 将 `$options.data` 数据做响应式处理 (`observe`) - 然后在我们使用的时候需要将 `$data`的数据绑定到 `this`上,并且做响应式处理(`proxy劫持`) - 接下来就是需要第一次渲染我们的页面 需要 Compiler 来帮助我们干活,将 `el`挂载节点和 `this`实例传给他 #### Compiler 需要做什么 ![image-20210526204813971](http://typora.coderyl.top/typora/image-20210526204813971.png) - 编译模板,递归遍历每一个节点`el.childNodes` - 当前节点有两种情况需要考虑,一个是 真实的 dom 元素,一个就是 文本元素 - 解析文本元素就是解析他这个文本是否包含了插值表达式 `node.nodeType === 3 && /**\{\{**(.*)**\}\}**/.test(node.textContent)` - 解析 dom 的时候就要注意是绑定了属性还是绑定了事件 - 做好之后需要更新 dom 元素的地方就进行第一次的初始化操作 --- 到了这里 页面的第一次渲染就做完了,数据也是响应式了,但是现在数据改变之后他不能进行更新,这个时候就要考虑如何进行更新了。 数据更新很重要的点 - 数据变了怎么通知到更新(`data->view`) - 一个数据变了到底哪些视图应该更新(`view->data`) ![image-20210526204904653](http://typora.coderyl.top/typora/image-20210526204904653.png) #### Watcher 需要干什么 - 依赖收集 > 视图中会⽤到 data 中某 key,这称为依赖。同⼀个 key 可能出现多次,每次都需要收集出来⽤⼀个 > Watcher 来维护它们,此过程称为依赖收集。 > 多个 Watcher 需要⼀个 Dep 来管理,需要更新时由 Dep 统⼀通知。 `Watcher` 内维护一个 `key`和`update`函数,在编译的时候,如果使用到了 key 的话我们就创建一个 `Watcher`,这就能 创建一个依赖,一般使用的话就是 差值表达式和动态绑定,在 Compiler 过程就可以将这个创建 watcher 的过程封装起来,而且初始化和更新视图的代码是一样的 就可以将 compiler 中的 修改视图的方法也封装起来做一个 更新视图函数传给 watcher 共用 #### Dep 需要干什么 dep 其实很简单,做一个 watcher 列表维护 依赖,在需要的时候将依赖遍历执行 他的 update 方法就行了。 --- 最难的一点来了,什么时候创建 Dep 以及如何将 watcher 和 dep 绑定起来? 什么时候创建 Dep? - 其实很简单,每一个劫持的数据都需要一个 Dep,进行依赖的管理 如何将 watcher 和 dep 绑定起来? 这个时候我们就想到了 observe 数据劫持的时候,每次调用 get 的时候是不是就是解析视图的时候。如果在`get`的时候获取到 `watcher`并将它与当前`动态劫持的数据`绑定起来,之后在 set 的时候让 dep 去通知跟新不就好了。 怎么在 get 的时候获取到 watcher 实例? 想到 watcher 的创建是在编译的时候,如果我们在 watcher 的构造函数内部主动的调用一次数据就能够触发 get,就能够把 watcher 实例和 get 过程进行一个连接,数据的传你我们是使用的类的静态变量,在使用数据之前将 watcher 实例绑定到 Dep 类上 ,使用完毕之后释放调他。在 get 过程中就可以获取到 watcher 实例了。 #### YLVue 类 ```js class YLVue { constructor(options) { // 保存选项 this.$options = options; this.$data = options.data; // 响应化处理 observe(this.$data); // 代理 吧 data中的key映射到 this上,并且 this[key] 和 this.$data[key] 做关联 proxy(this); // 编译 解析 dom 元素 将 this 和 dom 做一个关联 new Compiler(this.$options.el, this); } } ``` #### 数据代理 ```js function defineReactive(obj, key, val) { observe(val); // 每一次执行的时候都会创建一个管家 const dep = new Dep(); Object.defineProperty(obj, key, { get() { // 读数据的时候进行依赖收集 Dep.target && dep.addDep(Dep.target); return val; }, set(newVal) { if (newVal !== val) { observe(newVal); val = newVal; // 通知下去 dep.notify(); } }, }); } function observe(obj) { if (typeof obj !== 'object' || obj == null) { return; } new Observer(obj); } function proxy(vm) { Object.keys(vm.$data).forEach((key) => { Object.defineProperty(vm, key, { get() { return vm.$data[key]; }, set(val) { vm.$data[key] = val; }, }); }); } ``` #### Compiler 类 ```js class Compiler { // 一个元素,this实例 constructor(el, vm) { this.$el = document.querySelector(el); this.$vm = vm; // 编译模板 if (this.$el) { this.compile(this.$el); } } // 做编译的 分两个 编译 dom元素 和 编译属性 compile(el) { // childNodes 获取所有的节点 el.childNodes.forEach((node) => { // 判断类型 if (this.isElement(node)) { // console.log('编译元素 -->', node.nodeName); this.compileElement(node); } else if (this.isInter(node)) { // console.log('编译插值表达式 -->', node.textContent); this.compileText(node); } // 递归遍历 if (node.childNodes) { this.compile(node); } }); } // 对 差值文本进行编译 compileText(node) { this.update(node, RegExp.$1, 'text'); // node.textContent = this.$vm[RegExp.$1]; } // 对 dom 元素 进行编译 compileElement(node) { // 获取属性 const nodeAttrs = node.attributes; Array.from(nodeAttrs).forEach((attr) => { // console.log('attr -->', attr); const attrName = attr.name; // yl-xxx const exp = attr.value; // aaa if (this.isDirective(attrName)) { // yl-xxx = 'aaa' const dir = attrName.substring(3); // 执行这个指令 this[dir] && this[dir](node, exp); } else if (this.isEvent(attrName)) { // @click='methodName' const dir = attrName.substring(1); // 做事件监听 this.eventHandler(node, exp, dir); } }); } eventHandler(node, exp, dir) { node.addEventListener(dir, this.$vm.$options.methods[exp].bind(this.$vm)); } // yl-text 指令 text(node, exp) { this.update(node, exp, 'text'); } textUpdate(node, value) { node.textContent = value; } // yl-html 指令 html(node, exp) { this.update(node, exp, 'html'); } htmlUpdate(node, value) { node.innerHTML = value; } // yl-model 指令 model(node, exp) { // update 方法只完成的赋值 没有做事件监听 this.update(node, exp, 'model'); // input事件 node.addEventListener('input', (e) => { this.$vm[exp] = e.target.value; }); } modelUpdate(node, value) { node.value = value; } // 创建 Watcher 肯定是在编译的时候 使用到 key的时候就创建 // 定义一个和创建函数 来复用 // 所有的动态绑定都需要创建更新函数和对应的 Watcher update(node, exp, dir) { // 初始化 dir + 'Update' 这个指令对应的更新函数 // 更新函数的 value是当前最新的值 const fn = this[dir + 'Update']; fn && fn(node, this.$vm[exp]); // 更新 new Watcher(this.$vm, exp, (val) => { fn && fn(node, val); }); } // 判断一个属性是不是 自定义的指令 isDirective(attrName) { return /^yl-/.test(attrName); } // 判断一个属性是不是事件 isEvent(attrName) { return /^@/.test(attrName); } // 判断 是不是dom元素 isElement(node) { return node.nodeType === 1; } // 判断 是不是 插值表达式 isInter(node) { return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent); } } ``` #### Watcher 类 ```js class Watcher { // 实例 关联的key值 触发的更新函数 constructor(vm, key, updateFn) { this.$vm = vm; this.$key = key; this.$updateFn = updateFn; // 读一次数据 就会触发 defineReactive 中的get方法 Dep.target = this; this.$vm[key]; // 触发了get 就触发了依赖收集 Dep.target = null; } update() { this.$updateFn.call(this.$vm, this.$vm[this.$key]); } } ``` #### Dep 类 ```js class Dep { constructor() { this.deps = []; } addDep(watcher) { this.deps.push(watcher); } notify() { this.deps.forEach((watcher) => watcher.update()); } } ``` #### 总结一下 回过头来想一下 vue1 的实现挺符合我们的逻辑的,劫持数据 ,做动态绑定,依赖收集,数据改变了通知更新视图。 现在遇到了很严重的性能问题,每一次创建 的变量实在是太多了。 vue2 的优化。是每一个组件创建一个 watcher,但是组件内部的数据改变如何做动态更新呢? 这个时候就想到了 VNode 虚拟 dom,每一次数据改变 diff 算法比较两次差异,针对差异进行视图更新。 很多时候说 vue 抄 react 的虚拟 dom 的思想,但是 vue1 过渡到 vue2 是不得不使用这个方法的,是大的环境下催使我们不得不这样做。 ## 源码阅读 > vue 2.6.11 ### 调试环境搭建 - 安装依赖: npm i - 安装 rollup: npm i -g rollup - rollup 和 webpack 类似 都是代码打包工具,只不过 rollup 对纯 js 文件打包更加友好,轻量 - 修改 dev 脚本,添加 sourcemap,package.json ```json "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:webfull- dev", ``` - 运行开发命令: npm run dev ```js 这个时候报错了,百度是说rollup-plugin-alias对windows的兼容不好, https://blog.csdn.net/weixin_38659265/article/details/112004047 重新下了 rollup-plugin-alias ,在源码的node_modules 里面覆盖了 rollup-plugin-alias,重新npm i,npm run build,之后在编译源码就没问题了 ``` > 版本术语: > > runtime:仅包含运行时,不包含编译器 (vue.runtime.js) > ​ common:cjs 规范,用于 webpack1 (vue.common.js) > ​ esm:ES 模块,用于 webpack2+ (vue.esm.js) > ​ umd: universal module definition,兼容 cjs 和 amd,用于浏览器 (通常我们都省去 vue.js) 其实自习想想也很好理解,为什么要有这么多的版本,举个很简单的例子,写过 Vue 的都清楚,在浏览器中使用原生 js 写 vue 的时候我们挂载实例是用的 配置项中 el 选项,而在 vue-cli 中我们挂载实例是用 $mount 方法。 首先要理解 : - 浏览器执行的时候是浏览器环境 对应的版本是 umd - vue-cli 使用的环境是 webpack2+ 对应的是 对应的版本是 esm - 说道这两个了顺带解释一下 runtime 就是运行时 不含编译器,也就是 node 环境 ssr 我们现在考虑的是 浏览器的环境 那竟然环境不一样,使用的文件肯定也就不一样,打包的时候就应该区分, ```json "scripts": { "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev" } // 这里就说明了 scripts/config.js 这个是配置信息 TARGET:web-full-dev 使用的是 web-full-dev 对应的配置,也就是浏览器的环境 ``` 打开源码目录下的 scripts/config.js,找到 web-full-dev 对应的配置 ```json 'web-full-dev': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.js'), format: 'umd', env: 'development', alias: { he: './entity-decoder' }, banner }, // 可以看到 entry 指向的入口文件是 web/entry-runtime-with-compiler.js,resolve 是路径起别名,找到最上面定义的地方,还引入了一个 alias的文件 ,看一下就明白了啦 ``` OK,到这里环境就搭建完成了,也简单的介绍了环境的基本情况。 ### 初始化的流程 ![生命周期](http://typora.coderyl.top/typora/%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.png) ### new Vue的时候干了什么 #### 实例创建前的初始化工作 先找到我们打包前的入口文件 + `src\platforms\web\entry-runtime-with-compiler.js` + 扩展了`Vue`上$mount方法,判断 `options`中的`render`函数和 `template` ,`el`选项,最终是把 `el`变成 `template`,吧`template`变成`render`,保证了 `options`上一定有·函数,然后**执行了原本的 `$mount`方法** + 向上找 `Vue` + `src\platforms\web\runtime\index.js` + 在这个文件实现了 `$mount`方法,并且实现了 `__patch__`方法 + 向上找 Vue + `src\core\index.js` + 这个文件就是 初始化了 全局API `Vue.use/Vue.set/Vue.mixin`等等 + 向上找 Vue + `src\core\instance\index.js` + 这个文件就是真正的`Vue`构造函数实现的地方 + 这个文件还实现了很多 `Vue`实例的方法 ```js // 实例方法的初始化 initMixin(Vue); // 混入了 _init 方法 stateMixin(Vue); // $set/$del/$watch eventsMixin(Vue); // $on/$emit/$off/$once lifecycleMixin(Vue); // _update/$forceUpdate/$destroy renderMixin(Vue); // $nextTick/_render ``` 到这里 `Vue` 实例创建之前的初始化工作就实现了,主要是加强了`$mount`方法,初始化了 类方法,初始化了实例方法。 #### 实例的创建与挂载 + 第一步肯定是执行到 `src\core\instance\index.js` 中的 `Vue`方法进行创建实例 + 创建实例的时候有执行了 `_init()`方法 + `src\core\instance\init.js` + 合并了用户传递的选项和`Vue`原生的选项 + 实例进行 核心的数据初始化和挂载 ```js initLifecycle(vm); // 初始化生命有关的 $parent $root $childent initEvents(vm); // 初始化事件的监听 initRender(vm); // slots/$createElement callHook(vm, "beforeCreate"); // 组件创建之前的 钩子 initInjections(vm); // 注入 祖宗传入的 数据 initState(vm); // 组件的数据初始化 props/data/methonds/computed/watch initProvide(vm); // provide callHook(vm, "created"); if (vm.$options.el) { // 根据 el 去挂载 vm.$mount(vm.$options.el); } ``` + 调用了 实例的 `$mount` `src\platforms\web\runtime\index.js` 上面分析了 是在这个文件中 实现了 `$mount` 方法,这里的 `$mount`是被加强之后的了,所以会先执行加强的代码。 ```js Vue.prototype.$mount = function () { el = el && inBrowser ? query(el) : undefined; // 真正执行挂载的函数 return mountComponent(this, el, hydrating); }; ``` + 执行 `mountComponent` 函数, 发现这个函数是 在 `src\core\instance\lifecycle.js`文件中创建的,这个文件是在 `lifecycleMixin(Vue)` 过程中绑定了一个实例方法。 + 看看关键代码,走来是 `callHook(vm, "beforeMount");` + 紧接着创建了一个 `updateComponent` 函数 + 最最关键的就是 接下来 创建了一个 `Watcher` 观察者,这个观察者最最最重要的一个参数就是更新函数了,发现更新函数就是上面的 `updateComponent`,说明了现在初始化的时候会执行一次这个函数和之后只要数据一改变就会触发这个函数。这里说数据便就会发出其实不太准确,这个`Watcher`实际上是一个组件一个 `Watcher`只有组件内部的数据变化了才会触发`updateComponent`。 + 创建 `Watcher`的时候会执行一次 `updateComponent` 这个函数,我们现在看看这个 `updateComponent`干了什么 ```js // 调用 了 vm._update 方法,参数 是 vm._render() 的返回值 // vm._render() 执行之后会返回 一个 VNode 虚拟节点 updateComponent = () => { vm._update(vm._render(), hydrating); }; ``` + `_update`方法内部实现就是先判断是更新函数还是 第一次的渲染函数, ```js if (!prevVnode) { // 没有父元素就是第一次的渲染函数 vm.$el = vm.__patch__(vm.$el, vnode); } else { // 有父元素就是进行更新函数 vm.$el = vm.__patch__(prevVnode, vnode); } // __patch__ 的实现我们之后再看 ``` + 最后, 执行了一下 `callHook(vm, "mounted");`到这里,数据有了,真实的dom也有了 + 实例的创建步骤到这里就结束了 #### 小总结 在引入`Vue`的时候就会先执行一些 实例方法的初始化工作,在我们写代码 `new Vue`的时候就到了 `_init`方法的执行,然后就是执行了`$mount`的挂载,这里创建了一个 `Watcher`,执行了`updateComponent`去第一次执行了渲染函数 `_update`。到这里数据有了 真实`Dom`也有了 ### 数据的响应式 + `src\core\instance\init.js` 定义了`_init`方法 其中就有一个 `initState` 就是做数据响应式的 + `initState`中 初始化了 `props/methods/data/computed/watch` 我们这次关注一下 `initData` + `initData`中 先是判断了data是不是一个函数,是函数就获得函数的返回值 + 因为组件复用的话,他们的`data`是应用类型就有可以造成污染,就需要用函数进行隔离环境 + 在对 `data`中的所有的 `key`做一个安全性检查,让他们不能和`props/methods`中的重复 + 最后执行最关键的 代码 `observe(data);` + `src\core\observer\index.js`中定义了 `observe` 方法 + 判断是不是已经是响应式对象了(`__ob__`) ,判断是不是数组 + 是对象的话就 `ob = new Observer(value);` 创建一个 `Observer` 对象并且**返回**,这里的`value`是 传入的 **对象** + `new Observer` 的构造函数内部就会创建一个 `dep`,这里的`dep`对应的是这个对象的。 + 判断是数组还是对象,对象就调用 walk方法,对对象的每一个键值对进行 `defineReactive(obj, keys[i]);`处理 + `defineReactive` 内部又会创建一个 `dep`,此时这里的`dep` 对应的是当前的键(`key`)。 + `defineReactive` 内部显示判断用户有没有自定义 `getter/setter`,没有就使用`Object.defineProperty`对 对象的当前的`key`做`getter/setter` + 再用 get和set处理之前,先将当前键**对应的值** 在调用 `observe` 方法,并且获取返回值为 `childOb` + `get`内部,如果当前 有 `Dep.target(watcher)`,就把这个`watcher`和 当前`key` 的`dep`做关联,在判断当前的 `childOb`是否存在,存在就将 `childOb.dep`和 `watcher`也做一个关联,这里就做到了 当前对象改变,或者当前对象内部的元素进行了改变都会触发同一个 `watcher` + `set`内部,简单的对新的 `newVal`做一个响应式并且通知当前`key`管理的 `watcher`进行更新 + 数组的响应式也就是对能修改数组的7个方法做了增强,在这些方法执行的时候触发 `dep`的通知,并且其中有三个方法是改变数组元素的,要特殊处理这三个方法,对改变后的数组再次做响应式处理。 ### $set/$del/$watch #### $set ```js export function set() { // 判断是不是数组 以及下标是否合理 if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key); // splice 对应下标删除一个在添加一个,并且触发到了 管家的通知 target.splice(key, 1, val); return val; } // 判断当前对象有没有这个key if (key in target && !(key in Object.prototype)) { // 直接修改 会触发管家的通知 target[key] = val; return val; } // 如果当前对象不是响应式的数据 const ob = (target: any).__ob__; // 如果不是 我就直接添加值 if (!ob) { // 这里不会触发更新,因为判断了当前的对象不是响应式的数据 target[key] = val; return val; } // 如果是响应式的数据就在这调用一下 defineReactive(ob.value, key, val); // 主动的通知更新 ob.dep.notify(); return val; } ``` #### $del ```js export function del() { // 数组的判断 if (Array.isArray(target) && isValidArrayIndex(key)) { // 直接删除会触发响应式 target.splice(key, 1); return; } const ob = (target: any).__ob__; // 判断有没有这个值 if (!hasOwn(target, key)) { return; } delete target[key]; // 判断是不是 响应式的对象 if (!ob) { return; } // 是响应式的对象需要主动触发更新 ob.dep.notify(); } ``` #### $watch > vue 实例初始化的时候有一个 initWatch 底层就是调用的 $watch + initWatch ```js function initWatch(vm: Component, watch: Object) { for (const key in watch) { const handler = watch[key]; // 可以传回调函数数组? if (Array.isArray(handler)) { for (let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]); } } else { // 直接创建 Watcher createWatcher(vm, key, handler); } } } ``` + createWatcher ```js // 根据配置 创建一个 watch function createWatcher() { // 传入的是一个对象 if (isPlainObject(handler)) { // 获取配置 options = handler; // 获取处理函数 handler = handler.handler; } // 传入的是字符串 就去找 methods中的方法 if (typeof handler === "string") { handler = vm[handler]; } // expOrFn就是 要监听的键 return vm.$watch(expOrFn, handler, options); } ``` + $watch ```js Vue.prototype.$watch = function () { const vm = this; if (isPlainObject(cb)) { // 如果是一个对象的话就调用 createWatcher 方法 return createWatcher(vm, expOrFn, cb, options); } options = options || {}; // 标明是用户创建的 options.user = true; // 手动创建一个 Watcher const watcher = new Watcher(vm, expOrFn, cb, options); // 判断有没有配置 immediate 有会立即执行一次 if (options.immediate) { cb.call(vm, watcher.value); } // 返回一个 取消 watch的函数 return function unwatchFn() { watcher.teardown(); }; }; ``` ### 总结 代码执行到这里,我们的mounted生命周期也就执行完了,接下来就要进入 update有关的生命周期了 ### 浏览器同步代码和异步代码的执行逻辑 #### 异步更新队列 ![image-20210530175618248](http://typora.coderyl.top/typora/image-20210530175618248.png) + 事件循环Event Loop:浏览器为了协调事件处理、脚本执⾏、⽹络请求和渲染等任务⽽制定的⼯作机制。 + 宏任务Task:代表⼀个个离散的、独⽴的⼯作单元。浏览器完成⼀个宏任务,在下⼀个宏任务执⾏开始前,会对⻚⾯进⾏重新渲染。主要包括创建⽂档对象、解析HTML、执⾏主线JS代码以及各种事件如⻚⾯加载、输⼊、⽹络事件和定时器等。 + 微任务:微任务是更⼩的任务,是在当前宏任务执⾏结束后⽴即执⾏的任务。如果存在微任务,浏览器会清空微任务之后再重新渲染。微任务的例⼦有 Promise 回调函数、DOM变化等。 #### vue中的具体实现 ![image-20210530181229289](http://typora.coderyl.top/typora/image-20210530181229289.png) + 异步:只要侦听到数据变化,Vue 将开启⼀个队列,并缓冲在同⼀事件循环中发⽣的所有数据变更。 + 批量:如果同⼀个 watcher 被多次触发,只会被推⼊到队列中⼀次。去重对于避免不必要的计算和 DOM 操作是⾮常重要的。然后,在下⼀个的事件循环“tick”中,Vue 刷新队列执⾏实际⼯作。 + 异步策略:Vue 在内部对异步队列尝试使⽤原⽣的 Promise.then 、MutationObserver或setImmediate ,如果执⾏环境都不⽀持,则会采⽤ setTimeout 代替。**** ### 更新过程 + 数据发生改变 `dep`就会调用 `notify` 通知`watcher`发生更新 + 遍历每一个 `watcher`的 `update` 方法 + `update` 方法里面判断有没有配置 aync 配置了就立即执行,没有配置就 调用 `queueWatcher(this);`把这个watcher加入到队列中 + `src\core\observer\scheduler.js`里面同步定义了一个队列, `queueWatcher(this);` 就会吧 `watcher`实例加入到这个队列中,最后 调用了 `nextTick(flushSchedulerQueue);` + `flushSchedulerQueue`的实现是 调用 `queueWatcher` 队列中,所有的 `watcher`的`run`方法,run 干了两件事情 + 调用了 `get`方法,通过上下文我们就能知道 get就是之前挂载时候创建组件 `Watcher`传入的 `updateCompont`函数,进行渲染页面 + 如果watcher不是组件的实例而是用户自定义的 就 执行回调函数并且将新值和老值传给回调函数 ### nextTick的实现 + `src\core\util\next-tick.js`中又同步定义了一个队列 `callbacks`用来保存 使用 `nextTick`传入的回调函数(push的时候会做一个高阶函数,用来做错误捕获), 如果当前没有在 `pending`就会执行 `timerFunc`这个函数 + `timerFunc`是一个根据当前环境判断 能使用的使用的 微任务函数,调用了 `flushCallbacks`执行了 `callbacks`数组中的所有的函数。 > 我们平时也会主动调用 `nextTick`,这个时候 `nextTick` 就会吧我们传入的回调函数加入到 同步生成的 `callbacks` 回调函数数组中,在响应式数据改变的时候 会先有一个 `queuewatcher` 队列,然后是收集`watcher`,当`watcher`收集完成之后 将 `flushSchedulerQueue`刷新队列的方法加入到`nextTick` 的 `callbacks`函数中 #### 例题1 ```js const app = new Vue({ el: "#demo", data: { foo: "ready~~" }, mounted() { // 每一次的赋值都是 导致 watcher入队,赋值3次但是 watcher只入队了一次 this.foo = Math.random(); console.log("1:" + this.foo); this.foo = Math.random(); console.log("2:" + this.foo); this.foo = Math.random(); console.log("3:" + this.foo); // 异步行为,此时内容没变 console.log("p1.innerHTML:" + p1.innerHTML); // ready~~ // $nextTick 会把回调函数 放热 callbacks队尾 this.$nextTick(() => { // 这里才是最新的值 console.log("p1.innerHTML:" + p1.innerHTML); // 3 }); }, }); // 修改了 三次 foo,每一次都会 触发到setter,更新数据 导致 dep触发notify ,watcher就会被 update方法加入到 queuewatche队列中,但是这个队列只有一个 watcher,加入前会有id判断 // queuewatche 这个队列是同步代码 每一次改变都会判断是否要加入,更新队列的 flushSchedulerQueue才是异步代码 // 之后将 flushSchedulerQueue 加入到 nextTick 的 callbacks 回调函数中 // $nextTick又将 用户的回调函数加入到 callbacks中 // 然后实行 callbacks中的函数 先是执行了 flushSchedulerQueue 调用 watcher.run()刷新了视图 // 然后实行用户的回调函数 打印出了 3 ``` #### 例题2 ```js const app = new Vue({ el: "#demo", data: { foo: "ready~~" }, mounted() { // 每一次的赋值都是 导致 watcher入队,赋值3次但是 watcher只入队了一次 this.foo = Math.random(); console.log("1:" + this.foo); this.foo = Math.random(); console.log("2:" + this.foo); // $nextTick 会把回调函数 放热 callbacks队尾 this.$nextTick(() => { console.log("p1.innerHTML:" + p1.innerHTML); // 3 }); this.foo = Math.random(); console.log("3:" + this.foo); console.log("p1.innerHTML:" + p1.innerHTML); }, }); // 第一次给 foo赋值的时候就将 watcher加入到 queuewatcher中,就将 flushSchedulerQueue 加入到 callbacks中 // 每一次赋值只是调用 queuewatcher,并不改变 flushSchedulerQueue 在callbacks中的位置 // $nextTick 会将用户的 cb 加入 callbacks中 // 最后打印的时候执行顺序还是 flushSchedulerQueue cb ``` #### 例题3 ```js const app = new Vue({ el: "#demo", data: { foo: "ready~~" }, mounted() { this.$nextTick(() => { console.log("p1.innerHTML:" + p1.innerHTML); // ready~~ }); this.foo = Math.random(); console.log("1:" + this.foo); this.foo = Math.random(); console.log("2:" + this.foo); this.foo = Math.random(); console.log("3:" + this.foo); console.log("p1.innerHTML:" + p1.innerHTML); // ready~~ }, }); // 这里就不一样 了 因为 cb是最先加入到 callbacks中的, flushSchedulerQueue是后加入的 // 一开始视图没更新是 ready~~ 同步代码也是 ready~~ (数据是变了 ,但是视图是没有变的) ``` #### 例题4 ```js const app = new Vue({ el: "#demo", data: { foo: "ready~~" }, mounted() { this.foo = Math.random(); console.log("1:" + this.foo); this.foo = Math.random(); console.log("2:" + this.foo); this.foo = Math.random(); console.log("3:" + this.foo); console.log("p1.innerHTML:" + p1.innerHTML); // ready~~ new Promise().reverse().then(() => { console.log("Promise p1.innerHTML:" + p1.innerHTML); // 这个是最后输出 }); this.$nextTick(() => { console.log("p1.innerHTML:" + p1.innerHTML); // 3 }); } }); // callbacks 是同步创建的 new Promise() then的回调也是同步的 // 一开始改变foo就 收集watcher 然后将 flushSchedulerQueue 加入到 callbacks // 执行promise的时候 队列中就是 [callbacks, promise] // 执行 $nextTick 是 [[flushSchedulerQueue, cb], promise] // 所以输出结果就是 // ready~~ // p1.innerHTML:3 // Promise p1.innerHTML:3 ``` #### 总结 + 数据变化的时候 会触发 更新的方法(dep.nofify() -> watcher.update -> queueWatcher(this)) 这里就是先同步代码收集 watche 放入 queuewatcher队列中 ,加入的时候也会有去重操作 + 同步的将 flushSchedulerQueue 加入到 callbacks 异步函数队列中 + 数据改变 一个Watcher只会加一个 flushSchedulerQueue 到队列中 + flushSchedulerQueue 执行的时候会 遍历 queuewatche 队列,这个时候 queuewatche 队列可能被之前的同步代码修改很多次了 + flushSchedulerQueue 执行的时候是调用了 watcher的 get方法 也就是之前创建Watcher的时候传入的 updateComponent + flushSchedulerQueue 执行了 视图才会更新, 响应式的数据其实很早就变化了 + $nextTick 也就是向 callbacks 这个队列添加回调函数 + 异步函数队列中会 按顺序 执行函数队列中的方法 + callbacks 这个队列是同步创建的 这样 就会比 同步代码中其他的 微任务的cb先执行 ### 虚拟DOM https://www.processon.com/view/link/5e830387e4b0a2d87023890a#map 虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应⽤的各种状态变化会作⽤于虚拟DOM,最终映射到DOM上。 ![image-20210530212840761](http://typora.coderyl.top/typora/image-20210530212840761.png) #### 优点 + 减少了 dom的操作,patch也是得到最小的 dom操作量,配合异步更新就可以减少刷新,从而提升性能 + 跨平台,dom的操作,在不同的平台执行的操作都可以适配 + 兼容性,增强代码的兼容 #### 必要性 vue 1.0中有细粒度的数据变化侦测,它是不需要虚拟DOM的,但是细粒度造成了⼤量开销,这对于⼤型项⽬来说是不可接受的。因此,vue 2.0选择了中等粒度的解决⽅案,每⼀个组件⼀个watcher实例,这样状态变化时只能通知到组件,再通过引⼊虚拟DOM去进⾏⽐对和渲染。 #### Vue中的应用 + `src\platforms\web\runtime\index.js` 声明了 `$mount`方法 + `src\platforms\web\entry-runtime-with-compiler.js` 中对 `$mount`做了 `web` 端的加强 + `$mount` 最终是 调用了 `mountComponent` 方法,这个方法是创建了 `updateComponent` 方法并且创建了`Watcher` + `mountComponent` `src\core\instance\lifecycle.js` ```js updateComponent = () => { vm._update(vm._render(), hydrating); }; ``` #### _render 函数 + `src\core\instance\render.js` + _render 函数本质上就是 new Vue 传入的 `render`函数,在`src\platforms\web\entry-runtime-with-compiler.js`文件里面就会解析我们传入的 `el,template` 最终转换成 `render`函数 + 生成 虚拟dom ```js Vue.prototype._render = function () { const { render, _parentVnode } = vm.$options; } ``` #### _update 函数 + `src\core\instance\lifecycle.js` + 主要就是判断有没有 prevVnode(前一次的Vnode),从而进行第一次渲染操作还是之后的更新操作 + 将 Vnode转成正式的 dom ```js if (!prevVnode) { // 没有父元素就是第一次的渲染函数 vm.$el = vm.__patch__(vm.$el, vnode); } else { // 有父元素就是进行更新函数 vm.$el = vm.__patch__(prevVnode, vnode); } ``` #### `__patch__`函数 + 从上面分析过来,可以知道我们的 `__patch__`函数才是 最重要的,不同的平台对 `__patch__` 的实现不一样 + `src\platforms\web\runtime\index.js`中定义了 `__patch__`方法,它来自 `src\platforms\web\runtime\patch.js` + 比较两个 Vnode ,将新的和旧的比较,得出新的转换成老的 需要的最少 dom操作 ```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); // createPatchFunction 是一个工厂函数 通过传入的 nodeOps,和modules 来创建 patch函数 // nodeOps 就是 web 平台自己的 节点操作函数 // modules 就是 web 平台自己的 属性操作函数 // createPatchFunction 是Vue自己定义的 创建 patch函数的函数 export const patch = createPatchFunction({ nodeOps, modules }); ``` #### 流程 ```js watcher.run() => componentUpdate() => render() => update() => patch() ``` #### createPatchFunction 函数 这个是一个工厂函数,它返回了一个 `patch` 函数,这个 `patch` 函数其实就是 `Vue`的 `diff` 算法,`diff` 发生的地方是有一个 `patchVnode` 方法。 #### patch 的具体实现 + ⾸先进⾏树级别⽐较,可能有三种情况:增删改。 + new VNode不存在就删; + old VNode不存在就增; + 都存在就执⾏ patchVnode 执⾏ 比较操作; ![image-20210531145601116](http://typora.coderyl.top/typora/image-20210531145601116.png) > patch 只是简单的判断一下存不存在,都存在的情况才会调用 patchVnode 去比较两个节点 ##### patchVnode ⽐较两个`VNode`,包括三种类型操作:**属性更新、⽂本更新、⼦节点更新**,同层比较,深度优先。 0. 首先不管咋样 直接将新节点的属性直接更新到老节点上 ```js // 更新属性 在这里并不是判断 哪些属性是更新了的 而是直接一股脑进行属性比对更新 if (isDef(data) && isPatchable(vnode)) { // cbs 是所有的属性更新函数的集合 for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode); } ``` 1. 新⽼节点**均有**`children`⼦节点,则对⼦节点进⾏diff操作,调⽤`updateChildren` 2. 如果**新节点有⼦节点**⽽**⽼节点没有⼦节点**,先清空⽼节点的⽂本内容,然后为其新增⼦节点。 3. 当**新节点没有⼦节点**⽽**⽼节点有⼦节点**的时候,则移除该节点的所有⼦节点。 4. 当新⽼节点**都⽆**`children`⼦节点,只是⽂本的替换。 ```js // 一样的就直接退出 if (oldVnode === vnode) { return; } // 双方都有孩子 if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) // 重排 updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); // 新的有孩子 老的没有 } else if (isDef(ch)) { // 把老节点的文本清空 if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, ""); // 给老节点添加孩子 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); // 老的有孩子 行的没有 } else if (isDef(oldCh)) { // 移除掉老节点的孩子 removeVnodes(oldCh, 0, oldCh.length - 1); // 都没孩子 } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ""); } ``` > patchVnode 只是对当前的新老节点比较,有孩子就传入updateChildren 没有孩子就记性增删改查的操作 ##### updateChildren 因为传到这里的是新老节点的孩子列表,是一个数组,为了节省效率,`Vue`的做法很巧妙。 + 它 先获取到新老节点的首尾下标和首尾节点,(4个下标,4个节点) + 然后开始一层`while` 循环 先两个简单的if 判断调整一下可能出现的意外情况。 + 接着就比较两个头结点是不是一样的 是一样的就进行`patchVnode` 这两个头结点 进入下一层的 `while` + 如果头头比较不一样,就尾尾比较,一样就进行 `patchVnode` 这两个尾结点 进入下一层的 `while` + 如果尾尾比较不一样,就头尾比较,一样就进行 `patchVnode` 头尾两个结点 进入下一层的 `while` + 如果头尾比较不一样,就尾头比较,一样就进行 `patchVnode` 尾头两个结点 进入下一层的 `while` + 到这里 是 头头尾尾比较了4次 如果还不一样的话 他就将老节点的头结点 遍历新节点列表,看看能不能找到 + 找到了 就进行`patchVnode` 这两个节点,并且将位置移动 + 没找到就直接创建一个一模一样的节点 + 到这里了就是怎样都找不到 那就直接创建一个 节点全新的节点 + 这个时候 `while` 操作就完成了 + 最后进行收尾操作 如果新节点还剩下没比较的节点就全部创建增加到老节点上,如果老节点还剩下没有比较完的就全部删除 ```js // 创建首尾 4个游标索引 let oldStartIdx = 0; let oldEndIdx = oldCh.length - 1; let newStartIdx = 0; let newEndIdx = newCh.length - 1; // 获得首尾 4个节点(Vndoe) let oldStartVnode = oldCh[0]; let oldEndVnode = oldCh[oldEndIdx]; let newStartVnode = newCh[0]; let newEndVnode = newCh[newEndIdx]; // 开始游标要小于结束游标 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) {// 游标的调整操作 oldStartVnode = oldCh[++oldStartIdx]; } else if (isUndef(oldEndVnode)) {// 游标的调整操作 oldEndVnode = oldCh[--oldEndIdx]; } else if (sameVnode(oldStartVnode, newStartVnode)) { // 接下来是 头头比较 头尾比较 尾尾比较 尾头比较 patchVnode( oldStartVnode, newStartVnode); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode( oldStartVnode, newStartVnode); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode( oldStartVnode, newStartVnode); canMove && nodeOps.insertBefore( parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm) ); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode( oldStartVnode, newStartVnode); canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { // 不满足上面4种情况 // 新的取头一个 在老的中遍历查找 if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); // idxInOld是查到出来的索引 idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx); if (isUndef(idxInOld)) { // 没找到就创建一个新的 并且追加 createElm( newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx ); } else { // 找到了就 打补丁操作 vnodeToMove = oldCh[idxInOld]; if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode( vnodeToMove, newStartVnode, ); // 置空; oldCh[idxInOld] = undefined; // 移动操作 canMove && nodeOps.insertBefore( parentElm, vnodeToMove.elm, oldStartVnode.elm ); } else { // same key but different element. treat as new element // 创建 createElm( newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx ); } } newStartVnode = newCh[++newStartIdx]; } } // 首尾工作 if (oldStartIdx > oldEndIdx) { // 老数组先结束 批量增加 refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm; addVnodes( parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue ); } else if (newStartIdx > newEndIdx) { // 老数组后结束 批量删除 removeVnodes(oldCh, oldStartIdx, oldEndIdx); } ``` 有一个 比较函数 sameVnode 看一下这个函数的实现 ```js function sameVnode(a, b) { return ( a.key === b.key && ((a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b)) || (isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error))) ); } ``` #### 挂载 在patch 对新旧Vnode比较完成之后,会将dom挂载到页面上,这个时候就有一个很有趣的现象。 ![image-20210531191353797](http://typora.coderyl.top/typora/image-20210531191353797.png) 在 patch 的原码中(下面省去了一些) ```js if (isUndef(oldVnode)) { // 没有 旧节点 就直接创建一个 DOM createElm(vnode, insertedVnodeQueue); } else { const isRealElement = isDef(oldVnode.nodeType); // 老节点不是 dom 并且 新旧vnode一样 if (!isRealElement && sameVnode(oldVnode, vnode)) { // 更新的时候就会进来 因为这个时候 新旧都是虚拟dom // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly); } else { // 第一次创建的时候就会进来 有旧节点,没有新的Vnode // create new node 在页面上创建整个元素 createElm( vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ); // destroy old node 再删除掉旧的元素 if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0); } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode); } } } ``` #### 例题 ```js

{{a}}

const app = new Vue({ el: "#demo", data: { arr: ["a", "b", "c", "d"] }, mounted() { setTimeout(() => { this.arr.aplice(2, 0, "e"); }, 1000); } } ``` + 执行 `aplice` 操作的时候 `patch`函数的比较是怎么运行的 ```js // 使用key // aplice 之后 // oldVnode:[a b c d] // newVnode:[a b e c d] updateChildren 头头比较 ↓ // oldVnode:[b c d] // newVnode:[b e c d] updateChildren 头头比较 ↓ // oldVnode:[c d] // newVnode:[e c d] updateChildren 尾尾比较 ↓ // oldVnode:[c] // newVnode:[e c] updateChildren 尾尾比较 ↓ // oldVnode:[] updateChildren 头尾比较都比较完了 就创建了一个vnode 加入到 oldVnode // newVnode:[e] ``` 执行 `aplice` 操作的时候 如果标签上没有 `key`,`patch`函数的比较是怎么运行的 ```js // 不使用key 因为 sameVnode 会比较key 就出现了 undefined === undefined ->true // aplice 之后 // oldVnode:[a b c d] // newVnode:[a b e c d] updateChildren 头头比较 ↓ // oldVnode:[b c d] // newVnode:[b e c d] updateChildren 头头比较 ↓ // oldVnode:[c d] // newVnode:[e c d] updateChildren 头头比较 c e 本来是不一样 的 但是没有key就比较成了相等 就会传入 patchVnode(c,e)->修改所有的属性->updateChildren 继续递归调用比较子节点 增加了内存的消耗 // oldVnode:[c] // newVnode:[e c] updateChildren 头头比较 true patchVnode(c,e)->修改所有的属性->updateChildren // oldVnode:[] updateChildren 头尾比较都比较完了 就创建了一个vnode 加入到 oldVnode // newVnode:[e] ``` 可以看到 没有指定key的话,会认为不一样的节点是一样的,就继续 patchVnode -> 修改所有的属性 然后在updateChildren 递归调用,造成了很多不必要的性能浪费 #### 小总结 + `patch`方法是做两个节点之间的比较,都是针对老节点的操作 + 新节点不存在老节点存在 老节点就删除 + 新节点存在老节点不存在 老节点就创建 + 第一次创建`Vnode`的时候是 根据行的`Vnode`直接创建一个一模一样的`dom`节点,之后在将页面上的旧节点删除 + 如果新老节点都存在就调用 `patchVnode` + patch方法只是对比节点 有不一样的话就针对整个节点进行操作(增删创建,传入`patchVnode`),不会细微的调整节点是属性什么的 + `patchVnode`就是针对传入的两个节点进行操作 + 先不管三七二十一直接将这两个节点的属性更新成一样 + 获取这两个节点的孩子节点 + 如果都是文本节点就直接更新 + 如果有一方没有孩子就创建孩子节点或者删除这个孩子节点 + 都过都有孩子节点就调用`updateChildren`方法 + `patchVnode`先是对两个节点进行属性的修改,接着就是比较孩子节点,对孩子节点增删比对。 + `updateChildren`是获取了两个孩子节点的数组 + 头头,头尾,尾头,头尾比较,有一样的话就进行 `patchVnode` 操作 + 首尾没有的话就 取旧的节点的头结点在新节点中遍历,找到了一样的就 `patchVnode` + 没有找到一样的话就 `createElm` 进行创建操作 + 这个时候while就会执行完毕,最后进行收尾,对新旧节点比对剩下的做处理,新的多就添加,老得多就删除 + `updateChildren`是比较孩子节点数组,对这两个数组做一个高效的更新方法 ### 模板编译 模板编译的主要⽬标是将模板(template)转换为渲染函数(render) ![image-20210601144531320](http://typora.coderyl.top/typora/image-20210601144531320.png) `template => render()` 在 `src\platforms\web\entry-runtime-with-compiler.js` 文件里面 的 `compileToFunctions`函数 #### compileToFunctions ```js const { compile, compileToFunctions } = createCompiler(baseOptions) export const createCompiler = createCompilerCreator(function baseCompile(template, options) { // 解析 template 转换成 ast语法树 const ast = parse(template.trim(), options); if (options.optimize !== false) { // 优化 optimize(ast, options); } // ast => code string const code = generate(ast, options); return { ast, render: code.render, staticRenderFns: code.staticRenderFns, }; }); ``` #### parse 解析 template 转换成 ast语法树 ```js // 3个parse HTML / text / filter export function parse(template ,options) { // 解析过程 stack 是标签栈 开始标签就入栈 结束标签就出栈 //
const stack = []; // 核心解析算法 parseHTML(template, { warn, expectHTML, isUnaryTag, canBeLeftOpenTag, shouldDecodeNewlines, shouldDecodeNewlinesForHref, shouldKeepComment, outputSourceRange, // 解析 开始标签 start(tag, attrs, unary, start, end) { // ... // 创建 ast对象 let element: ASTElement = createASTElement(tag, attrs, currentParent); // ... if (inVPre) { // 解析属性 processRawAttrs(element); } else if (!element.processed) { // 结构型的指令 v-for processFor(element); // 结构型的指令 v-if processIf(element); processOnce(element); } if (!unary) { // 入栈 stack.push(element); } else { // 自闭和标签 closeElement(element); } }, // 解析 结束标签 end(tag, start, end) { // pop stack stack.length -= 1; closeElement(element); }, // 文本解析 主要是 {{}} chars(text: string, start: number, end: number) { // ... }, // 注释的解析 comment(text: string, start, end) { // ... }, }); return root; } ``` #### optimize 将刚刚生成的 ast语法树进行优化,比如 如果节点是纯HTML会标记一下 ```js // 优化 export function optimize(root, options) { if (!root) return; // 标记静态节点 markStatic(root); // 标记静态的根节点 markStaticRoots(root, false); } ``` #### generate 将生成的 ast 语法树变成 code 代码字符串 ```js export function generate(ast, options) { const state = new CodegenState(options); // 解析过程 const code = ast ? genElement(ast, state) : '_c("div")'; return { render: `with(this){return ${code}}`, staticRenderFns: state.staticRenderFns, }; } export function genElement(el, state) { // 静态的 if (el.staticRoot && !el.staticProcessed) { return genStatic(el, state); } else if (el.once && !el.onceProcessed) { return genOnce(el, state); // v-for } else if (el.for && !el.forProcessed) { return genFor(el, state); // v-if } else if (el.if && !el.ifProcessed) { return genIf(el, state); } else if (el.tag === "template" && !el.slotTarget && !state.pre) { return genChildren(el, state) || "void 0"; } else if (el.tag === "slot") { return genSlot(el, state); } else { // component or element let code; if (el.component) { code = genComponent(el.component, el, state); } else { let data; if (!el.plain || (el.pre && state.maybeComponent(el))) { data = genData(el, state); } const children = el.inlineTemplate ? null : genChildren(el, state, true); code = `_c('${el.tag}'${ data ? `,${data}` : "" // data }${ children ? `,${children}` : "" // children })`; } // module transforms for (let i = 0; i < state.transforms.length; i++) { code = state.transforms[i](el, code); } return code; } } ``` #### 小结 + parse方法 将template字符串转成 -> ast语法树 + 这里就会解析v-for 解析 v-if 解析差值表达式等等 + optimize方法 优化 ast语法树 将一些纯静态的 html代码标记,这样下次就可以节约时间了 + generate方法 将生成的 ast语法树 转成 渲染函数字符串 #### v-for的解析 + `parser/index.js` `processFor` 方法 模板转act语法树 v-for="s in arr" 解析结果是 for:'arr' alias:'s' ![image-20210602190859508](http://typora.coderyl.top/typora/image-20210602190859508.png) + `src\compiler\codegen\index.js` `genFor ` 方法 ast语法树转代码字符串 代码⽣成 结果 注意有一个 _l() 方法 ```js "with(this){return _c('div',{attrs:{"id":"demo"}},[_m(0),_v(" "),(foo)?_c('p',[_v(_s(foo))]):_e(),_v(" "),_l((arr),function(s){return _c('b',{key:s},[_v(_s(s))])}) ,_v(" "),_c('comp')],2)}" ``` #### v-if 解析 + `parser/index.js` `processIf` 方法 模板转act语法树 ![image-20210602191224450](http://typora.coderyl.top/typora/image-20210602191224450.png) + `src\compiler\codegen\index.js` `genIfCondition ` 方法 ast语法树转代码字符串 代码⽣成 结果 ```js "with(this){return _c('div',{attrs:{"id":"demo"}},[(foo) ? _c('h1',[_v(_s(foo))]) : c('h1',[_v("no title")]),_v(" "),_c('abc')],1)}" ``` #### v-for中为什么不能使用 v-if 编译过程中我们可以知道 v-for 的解析 先于 v-if , v-for 会遍历生成一个很大的ast语法树,之后解析v-if 的时候在这个很大的语法树中每一项又要加上v-if 的ast语法树的条件,就会十分的浪费性能。 #### 下划线函数到底是什么 `src\core\instance\render-helpers\index.js` ```js export function installRenderHelpers(target: any) { target._o = markOnce; target._s = toString; target._l = renderList; // v-for target._t = renderSlot; // Slot target._f = resolveFilter; target._v = createTextVNode; // text 普通文本 target._p = prependModifier; ...... } // 最重要的 _c 是在 initRender中混入的 src\core\instance\render.js _c 就是h函数 ``` ### 组件化机制 父组件--> 子组件 创建时期:父组件-->子组件 挂载时期:子组件-->父组件 #### Vue.components() 声明全局组件 Vue有三个全局方法 "component",组件 "directive",指令 "filter"过滤器 + src\core\index.js 中 initGlobalAPI(Vue); 注册了全局API + initGlobalAPI 中 ```js // src\shared\constants.js export const ASSET_TYPES = ["component", "directive", "filter"]; // src\core\global-api\index.js import { ASSET_TYPES } from "shared/constants"; // 声明这三个 全局方法 ASSET_TYPES.forEach((type) => { Vue.options[type + "s"] = Object.create(null); }); // 实现这三个 全局方法 initAssetRegisters(Vue); ``` + src\core\global-api\assets.js initAssetRegisters 方法 ```js /* @flow */ import { ASSET_TYPES } from "shared/constants"; import { isPlainObject } from "../util/index"; export function initAssetRegisters(Vue: GlobalAPI) { ASSET_TYPES.forEach((type) => { Vue[type] = function (id: string, definition: Function | Object): Function | Object | void { if (!definition) { return this.options[type + "s"][id]; } else { // 是一个组件 definition是一个纯对象 if (type === "component" && isPlainObject(definition)) { definition.name = definition.name || id; // extend 全局注册 definition = this.options._base.extend(definition); } if (type === "directive" && typeof definition === "function") { definition = { bind: definition, update: definition }; } // 全局注册,每一个 vue 的实例下都有了 这个配置 this.options[type + "s"][id] = definition; return definition; } }; }); } ``` #### 全局组件和局部组件的区别 ```js // Vue.components方法就是会注册成全局组件 // 原理就是 调用了extend 方法 给所有的Vue实例的 options都配置上 了这个组件 // 局部组件就是只有这个组件的配置对象的 options上有 的components ``` #### 组件的实例化和挂载 ##### 整体流程 ```js // 走到 createElement 的时候就会区别对待 是组件还是普通的HTML标签 new Vue() => $mount() => updateComponent() => vm._render() => createElement() => createComponent() => vm._update() => patch() => createElm() => createComponent() ``` ##### _render -> createElement() + src\core\vdom\create-element.js ```js export function _createElement(context,tag,data, children, normalizationType){ // 开始 根据传入的 tag的类型做对应的处理 let vnode, ns; if (typeof tag === "string") { let Ctor; ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag); // 判断他是不是 原生的 标签 if (config.isReservedTag(tag)) { // parsePlatformTagName 解析标签 vnode = new VNode(config.parsePlatformTagName(tag), data, children, undefined, undefined, context); // 判断是否存在 Ctor 构造函数,从全局的 components配置上找 } else if ((!data || !data.pre) && isDef((Ctor = resolveAsset(context.$options, "components", tag)))) { // 自定义的 component vnode = createComponent(Ctor, data, context, children, tag); } else { vnode = new VNode(tag, data, children, undefined, undefined, context); } } else { vnode = createComponent(tag, data, context, children); } } ``` ##### _render -> createComponent() + src\core\vdom\create-componentjs ```js // 创建自定义组件的 虚拟dom的方法 export function createComponent( Ctor,data , context , children , tag ) { // 组件的参数兼容性处理。。。 // 这里开始处理组件的 data data = data || {}; resolveConstructorOptions(Ctor); // v-model的处理 if (isDef(data.model)) { transformModel(Ctor.options, data); } // 属性的抽取 const propsData = extractPropsFromVNodeData(data, Ctor, tag); // 事件监听 自定义事件 const listeners = data.on; // 事件监听 原生事件 data.on = data.nativeOn; // 安装组件 hooks:init,insert。。 // 现在是生产 ast语法树 并不是生成真正的 dom 是在 patch中生成真正的 dom但是我们在这个环节就准备好patch中需要的一些操作 installComponentHooks(data); const name = Ctor.options.name || tag; // 创建组件的Vnode // vue-component-1-name const vnode = new VNode(...); return vnode; } ``` ##### _update -> patch -> createElm() ```js function patch(oldVnode, vnode){ if (isUndef(oldVnode)) { // 如果老的不存在就直接 创建一个新的 dom createElm(vnode, insertedVnodeQueue); } } // createElm function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) { // 判断当前的 Vnode是否是一个自定义组件的Vnode if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return; } // 原生标签 const data = vnode.data; const children = vnode.children; const tag = vnode.tag; if (isDef(tag)) { // 是原生标签 // 创建 vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode); setScope(vnode); createChildren(vnode, children, insertedVnodeQueue); if (isDef(data)) { // 插入初始化的 hooks 初始化标签属性,事件,样式等等 invokeCreateHooks(vnode, insertedVnodeQueue); } // 插入到父节点下 insert(parentElm, vnode.elm, refElm); } else if (isTrue(vnode.isComment)) { // 是注释 vnode.elm = nodeOps.createComment(vnode.text); insert(parentElm, vnode.elm, refElm); } else { // 是文本 vnode.elm = nodeOps.createTextNode(vnode.text); insert(parentElm, vnode.elm, refElm); } } ``` ##### _update -> patch -> createComponent() ```js // 创建一个自定义组件的 dom结构 function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) { // 获取 hook let i = vnode.data; if (isDef(i)) { const isReactivated = isDef(vnode.componentInstance) && i.keepAlive; // 初始化的 钩子就执行了 initHooks中实现了实例化和挂载 if (isDef((i = i.hook)) && isDef((i = i.init))) { i(vnode, false /* hydrating */); } if (isDef(vnode.componentInstance)) { // 属性的属性 initComponent(vnode, insertedVnodeQueue); // 追加到父组件中 insert(parentElm, vnode.elm, refElm); if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); } return true; } } } ``` ##### 小总结 + 引入Vue的时候 就会全局注册一些API 其中就包括 components 方法 + Vue根实例优先创建 + 根实例执行了 $mount() 方法 + 创建了updateComponent方法 执行 `vm._update(vm._render());` `_render`是 模板=>Vnode `_update` 是 Vnode=>真实DOM + `_render()`内部 这个是生成 Vnode + vnode = render.call(vm._renderProxy, vm.$createElement); 就将vm.$createElement作为 h函数 执行了 + 执行 createElement方法 见上面详解,是组件的话就会调用 createComponent 方法 + createComponent 中 很重要的就是 `installComponentHooks` 准备了一下hooks,并且new VNode(...) 创建了一个Vnode + `_update(Vnode)`内部 patch操作 将传入的Vnode 转成 真实的 DOM + 第一次 的时候会执行 createElm 方法 + 是元素dom就创建原生的标签,并且将这个标签插入父节点 + 是组件就 调用 createComponent 方法创建组件 主要就是调用了 hooks中的init hooks + init hook就会执行 $mount 挂载子组件,就会从头开始又执行这个步骤 + 还有一些收尾操作,初始化一些属性,将子组件插入到父组件中 ## 事件的处理 样例代码 ```HTML

事件处理机制

this is p

``` 编译结果 ```js ƒ anonymous() { with(this){return _c('div',{attrs:{"id":"demo"}},[ _c('h1',[_v("事件处理机制")]),_v(" "), _c('p',{on:{"click":onClick}},[_v("this is p")]),_v(" "), _c('comp',{on:{"myclick":onMyClick}})],1)} } ``` 在编译的过程中,两者的差异并不大,说明两者最大的差异是在之后的解析过程。 解析过程就想到了 `patch` 中的 `createElm` 方法 ### 普通事件 patch() => createElm() => invokeCreateHooks() 判断是不是原生标签 ,是的话就创建这个标签,在处理这个标签的孩子,在去处理这个标签自己的事情(invokeCreateHooks方法) ```js function invokeCreateHooks(vnode, insertedVnodeQueue) { // 每次都执行所有的回调 cbs 是我们一开始vdom预先定义好的全部处理方法 // 在这个 create里面就会有 事件的处理 for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode); } } ``` + `src\platforms\web\runtime\modules\events.js` `updateDOMListeners()` ```js function updateDOMListeners(oldVnode, vnode) { const on = vnode.data.on || {}; const oldOn = oldVnode.data.on || {}; target = vnode.elm; // 一些常规的操作 normalizeEvents(on); // 添加时间监听或者更新 updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context); target = undefined; } ``` + `src\core\vdom\helpers\update-listeners.js` `updateListeners()` ```js function updateListeners(...){ // 做了很多兼容性处理和bug修复 最重要的就是 调用了add方法 add(event.name, cur, event.capture, event.passive, event.params); } // add 方法是外面传入的 也就是 updateDOMListeners 传入的 function add(){ target.addEventListener(name, handler, supportsPassive ? { capture, passive } : capture); } ``` ### 自定义事件 patch() => createElm() => createComponent()(这个是将vnode转dom) => (这个是编译生成的createComponent中定义的hook) => createComponentInstanceForVnode() (这个就是创建一个实例了) => _init()(就是创建实例的时候执行的初始化方法) => initEvents() => updateComponentListeners() `hook.init()` 会创建这个实例以及挂载,创建了实例的时候就会执行 _init() + src\core\instance\init.js `_inti()` + src\core\instance\events.js `initEvents()` ```js function initEvents(vm) { vm._events = Object.create(null); vm._hasHookEvent = false; // init parent attached events // 爸爸的事件函数 在孩子监听到之后让孩子执行 const listeners = vm.$options._parentListeners; if (listeners) { // 更新组件的事件管理 updateComponentListeners(vm, listeners); } } ``` + src\core\instance\events.js `updateComponentListeners()` ```js export function updateComponentListeners(vm, listeners, oldListeners) { target = vm; // 在这里和上面一样 执行了 updateListeners 方法 // 看过上面就知道他是调用了 传入的 add 方法 来绑定他的事件 updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm); target = undefined; } // 组件事件的 add 方法 function add(event, fn) { target.$on(event, fn); } ``` ### 相关API #### $on() + src\core\instance\events.js ```js Vue.prototype.$on = function (event, fn){ const vm = this; if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { vm.$on(event[i], fn); } } else { // 在 _events 数组中添加事件 (vm._events[event] || (vm._events[event] = [])).push(fn); } return vm; }; ``` #### $emit() ```js Vue.prototype.$emit = function (event: string): Component { const vm: Component = this; // 获取回调函数数组 let cbs = vm._events[event]; if (cbs) { cbs = cbs.length > 1 ? toArray(cbs) : cbs; const args = toArray(arguments, 1); const info = `event handler for "${event}"`; // 遍历调用 for (let i = 0, l = cbs.length; i < l; i++) { invokeWithErrorHandling(cbs[i], vm, args, vm, info); } } return vm; }; ``` #### $once() ```js Vue.prototype.$once = function (event: string, fn: Function): Component { const vm: Component = this; // 封装了一个高阶函数 取消订阅并且执行一次这个函数 function on() { vm.$off(event, on); fn.apply(vm, arguments); } on.fn = fn; // 监听这个高阶函数 vm.$on(event, on); return vm; }; ``` #### $off() 有点复杂,就是对参数的所有可能情况处理 ```js Vue.prototype.$off = function (event?: string | Array, fn?: Function): Component { const vm: Component = this; // 没有传入参数就取消所有的事件 if (!arguments.length) { vm._events = Object.create(null); return vm; } // 事件名称是一个数值 遍历取消 if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { vm.$off(event[i], fn); } return vm; } // 只传入事件名称 const cbs = vm._events[event]; if (!cbs) { return vm; } // 没有指定回调函数就取消所有的回调函数 if (!fn) { vm._events[event] = null; return vm; } // 指定了回调函数就取消指定的回调函数 let cb; let i = cbs.length; while (i--) { cb = cbs[i]; if (cb === fn || cb.fn === fn) { // 查找 cbs.splice(i, 1); // 删除 break; } } return vm; }; ``` ### 生命周期hook事件 在任何一个组件的生命周期中,我们都可以在这个标签上写点东西,让他生命周期在执行的时候也执行我们定义的函数。 例:有一个来自第三方的复杂表格组件,表格进行数据更新的时候渲染时间需要1s,由于渲染时间较长,为了更好的用户体验,我希望在表格进行更新时显示一个loading动画。修改源码这个方案很不优雅。 ```html
``` 实现原理 + $on的时候多加了一步 ```js Vue.prototype.$on = function(){ // 匹配 hook的正则 const hookRE = /^hook:/; if (hookRE.test(event)) { // 匹配到了就添加一个标记 vm._hasHookEvent = true; } } ``` + 调用生命周期 是使用了 callHook(vm, "lifeName"); ```js export function callHook(vm: Component, hook: string) { // 做触发生命周期的操作。。。。 // 判断标记 if (vm._hasHookEvent) { // 触发 vm.$emit("hook:" + hook); } } ``` 很多时候会说,在生命周期的时候触发事件我写在对应的生命周期中调用这个方法不就好了,但是很多时候我们用的是别人写好的组件,我们又不好去修改别人的代码 只能用这样的方式去插入一些代码 ### v-model原理 例子 ```html

双向绑定机制

``` 编译结果 ```js // input _c('input', { directives: [{ name: "model", rawName: "v-model", value: (foo), expression: "foo" }], attrs: { "type": "text" }, domProps: { "value": (foo) }, on: { "input": function($event) { if ($event.target.composing) return; foo = $event.target.value } } }) // comp _c('comp', { model: { value: (foo), callback: function($$v) {foo = $$v}, expression: "foo" } }) ``` 我们发现自定义组件和原生dom的 v-model的render是不一样的 #### 原生标签 原生标签就是会在 patch => createElm => invokeCreateHooks 处理绑定的value的值和处理原生的 input事件 原生input中会有很多的type类型,但是使用v-model的时候就不会冲突是因为vue内部帮我们优化了 ```js // input _c('input', { directives: [{ name: "model", rawName: "v-model", value: (foo), expression: "foo" }], attrs: { "type": "text" }, domProps: { "value": (foo) }, on: { "input": function($event) { if ($event.target.composing) return; foo = $event.target.value } } }) ``` 原生标签的编译结果会有 directives 中指明了 model 这个model在编译阶段生成是 平台特有的 所以代码就是在 `src\platforms\web\compiler\directives\model.js`中 ```js if (el.component) { genComponentModel(el, value, modifiers); // component v-model doesn't need extra runtime return false; } else if (tag === "select") { genSelect(el, value, modifiers); } else if (tag === "input" && type === "checkbox") { genCheckboxModel(el, value, modifiers); } else if (tag === "input" && type === "radio") { genRadioModel(el, value, modifiers); } else if (tag === "input" || tag === "textarea") { genDefaultModel(el, value, modifiers); } else if (!config.isReservedTag(tag)) { genComponentModel(el, value, modifiers); // component v-model doesn't need extra runtime return false; } ``` #### 自定义组件 + src\core\vdom\create-component.js `createComponent()` ```js // v-model的处理 生成的Vnode在会有一个 model项 // comp _c('comp', { model: { value: (foo), callback: function($$v) {foo = $$v}, expression: "foo" } }) // createComponent 中对v-model的处理 if (isDef(data.model)) { transformModel(Ctor.options, data); } ``` + transformModel() ```js // v-model 的实现 function transformModel(options, data: any) { // 获取 v-model 绑定的值 v-model兼容了多种input的类型就是因为这里判断了一下 options.model.prop如果在编译的生成的vnode中有 prop选项就用prop 没有就默认value const prop = (options.model && options.model.prop) || "value"; // 获取 v-model 绑定的事件 const event = (options.model && options.model.event) || "input"; // 给data定义一个 prop 值是 data.model.value (data.attrs || (data.attrs = {}))[prop] = data.model.value; // 创建事件监听 const on = data.on || (data.on = {}); const existing = on[event]; const callback = data.model.callback; // 做事件的绑定 冲突的话会合并 if (isDef(existing)) { if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback) { on[event] = [callback].concat(existing); } } else { on[event] = callback; } } ``` 自定义的组件创建过程中之后就又是 原生事件的绑定