企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
Vue.js的核心是**数据驱动**。指**视图是由数据驱动生成的**,我们**对视图的修改,不会直接操作DOM,而是通过修改数据**。 与传统前端(jQuery):大大简化了代码量,逻辑变得清晰,非常利于维护。只关心数据的变化会使逻辑变得非常清晰。 # new Vue发生了什么 new 实例化是一个对象。Vue只能通过new关键字初始化,然后调用**this._init**方法 Vue初始化主要就干了几件事:**合并配置,初始化生命周期,初始化事件中心,初始化render,初始化data、props、computed、watcher等**。 ~~~ Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to avoid this being observed vm._isVue = true // merge options 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. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) } if (vm.$options.el) { vm.$mount(vm.$options.el) } } ~~~ ***** **当 new Vue的时候:** 执行this._init:一堆初始化工作 1. **合并options:将所有传入的options,merge到$options上。** 所有可以通过vm.$options.el,访问代码的el,vm.$options.data. 2. 初始化一堆内容:**初始化生命周期,初始化事件中心,初始化render,初始化data、props、computed、watcher等** 3. 初始化之后,判断当前options是否有el(字符串),会调用**vm.$mount**方法,挂载页面。 调试源码方法:debugger **this.message能访问到?** initState(vm)中initData data:是一个函数, vm._data = data **所有的data,methods,computed的内容,最终都会挂载到vm上。**,可以通过this方法。 通过proxy函数实现,定义getter和setter,通过defineProperty方法代理key ~~~ proxy(vm, '_data', key) ~~~ **通过proxy做了一层代理:当调用了this.message时,实际上调用了this._data.message,即访问this.data.message。** **此时还做了响应式的处理。** # Vue实例挂载的实现(vm.$mount) Vue中我们**通过$mount实例方法去挂载vm的,**因为$mount这个方法的实现和平台、构建方式都相关,重点分析compiler版本的$mount实现,抛开webpakc的**vue-loader**,我们在纯前端浏览器环境分析Vue的工作原理。 分析代码: * 首先,它对el做了限制,Vue不能挂载在**body,html**这样的根节点上(因为**会覆盖**)。 * **如果没有定义render方法**,则会把**el**或则**template字符串**转换成**render方法**。(Vue2.0版本中,所有Vue的组件的渲染都需要render方法,无论我们用单文件.vue开发,还是写了el或则template属性,最终都会转换成render方法),这个过程是vue的一个“在线编译”的过程。**Vue最终只认render函数。** * 调用原先**原型上的$mount方法挂载**。 $mount 方法支持传入2个参数,一个是el(挂载的元素,字符串或dom对象),第二个参数和服务端渲染有关。 $mount方法实际上会去调用mountComponent方法,**核心是先实例化一个渲染Watcher**,在它的回调函数中**会调用updateComponent方法**,在此方法中**调用`vm._render`方法先生成虚拟Node,最终调用`vm._update`更新DOM。** **new Watcher(渲染Watcher)**,实际上是一个观察者模式。 在这里两个作用: 1. 初始化的时候会执行回调函数 2. 当vm实例中的检测的数据发生变化的时候执行回调函数。 函数最后判断为根节点的时候设置`vm._isMounted`为`true`, 表示这个实例已经挂载了,同时执行`mounted`钩子函数。 **梳理:** 拿到render函数(**template编译转换的函数(调用vm._c)或手写render(调用vm.$createElement)**),调用$mount方法(mountComponent方法(定义了updateComponent函数,是个渲染watcher,实际上执行了真实的渲染,除了了首次,还有监听更新。入口都是updateComponent)), # render Vue的_render方式是实例的一个私有方法,用来**把实例渲染成一个虚拟Node**。返回一个Vnode。 我们平时开发中手写render方法的场 景比较少,而写的比较多的是template模板,在之前的**mounted方法实现中,会把template编译成render方法**,这个编译过程非常复杂。 render函数的第一个参数是createElement,就是vm.$createElement. 实际上vm.$createElement方法定义在执行**initRender方法**的时候,可以看到除了`vm.$createElement`方法,还有一个`vm._c`方法,它是被模板编译成的render函数使用,而**vm.$createElement是用户手写render方法使用的**,这两个方法制成的参数相同,并且内部都调用了ceateElement方法。 **vm._render最终通过执行createElement方法并返回的是vnode,它是一个虚拟Node。** ~~~ render(createElement){ return createElement('div',{ attrs:{ id:'#app1', } }, this.$message) } ~~~ 根节点只能有一个Vnode, # Virtual DOM 产生的前提是**浏览器中DOM是很“昂贵”的**。真正的DOM元素是非常庞大的,因为浏览器的标准就把**DOM设计的非常复杂**。当我们**频繁的去做DOM更新**,会产生**一定的性能问题**。 Virtual DOM(VNode)就是用一个原生**JS对象去描述一个DOM节点**,所以它比**创建一个DOM的代价要小很多**。 Virtual DOM使用VNode这么一个Class去描述。实际上Vue.js中Virtual DOM是**借鉴了一个开源库snabbdom的实现**。然后加入了一些Vue.js特色的东西。 其实VNode是对**真实DOM的一种抽象描述**,它的核心定义无非就是几个**关键属性**,标签名、数据、子节点、键值、class属性等,其他属性都是用来扩展VNode的灵活性以及实现一些特殊feature的。**由于VNode只是用来映射到真实DOM的渲染,不需要包含操作DOM的方法,因此它是非常轻量和简单的**。 Virtual DOM除了它的**数据结构的定义**,映射到真实的DOM实际上要经历**VNode的create、diff、patch等过程**。 那么在Vue.js中,VNode的**create是通过之前提到的createElement方法创建的**。 # createElement render函数(**template编译转换的函数(调用vm._c)或手写render(调用vm.$createElement)**),这两个方法**都会调用createElement函数**。 Vue.js利用createElement方法创建VNode,createElement方法实际上是对`_createElement`方法的封装,它允许传入的参数更加灵活,**在处理这些参数后,调用真正创建VNode的函数_createElement** ~~~ ~~~ _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> ~~~ ~~~ _createElement方法有5个参数,context表示VNode的上下文环境,它是Component类型;tag表示标签,它可以是一个字符串,也可以是一个Component,data表示VNode数据,children表示当前VNode的子节点,它是任意类型的,它接下来需要被规范为标准的VNode数组,normalizationType表示子节点规范的类型,类型不同规范的方法也就不一样。 **具有属性__ob__,说明是响应式的。** isPrimitive:是否是基础类型 ## children的规范化 **由于Virtual DOM实际上是一个树状结构,每一个VNode可能会有若干个子节点,这些子节点应该也是VNode类型。** 经过对children的规范化,children变成了一个类型为VNode的Array。 ## VNode的创建 回到createElement函数,规范化children后,接下来会去创建一个VNode的实例。 总结: createElement创建VNode的过程:**每个VNode有children,children每个元素也是一个VNode,这样就形成了一个VNode Tree,它很好的描述了我们的DOM Tree。** 回到mountComponent函数的过程,我们已经知道**vm._render是如何创建了一个VNode**,接下来就是把这个**VNode渲染成一个真实的DOM并渲染出来,这个过程通过vm._update完成**。 # update Vue的_update是实例的一个私有方法(原型方法),它被调用的时机有2个:**一个是首次渲染,一个是数据更新的时候**。 本次只分析首次渲染,数据更新部分在之后的分析响应式原理时涉及。 **_update方法的作用是把VNode渲染成真实的DOM** **_update的核心就是调用`vm.__patch__`方法**,这个方法实际上在不同平台定义是不一样的。是否是服务器渲染也会对这个方法产生影响。因为在服务端渲染中,没有真实的浏览器DOM环境,所以不需要把VNode最终转换成DOM,因此是一个空函数。 ~~~ function patch (oldVnode, vnode, hydrating, removeOnly) ~~~ **patch方法**:接收4个参数,**oldVnode**表示旧的**VNode节点**,它也可以**不存在或者是一个DOM对象**;**vnode**表示执行_render后返回VNode的节点;**hydrating**表示是否是服务端渲染;**removeOnly**是给transition-group用的。 我们在vm._update的方法是这么调用patch方法的: **函数科里化技巧,一次性传入,每次调用pathc时,不用关心差异化,因为第一次调用的时候已经存起来了** ~~~ // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) ~~~ 首次渲染场景: 在执行path函数的时候,传入的vm.$el对应的是例子中id为app的DOM对象,vm.$el的赋值是在之前mountComponent函数做的,vnode对应的是render函数里的返回值。hydrating在非服务端情况下渲染为false,removeOnly为false。 我们传入的oldVnode实际上是一个DOM container, 把oldVnode转化成VNode对象,然后再调用createElm方法,这个方法非常重要: ~~~ function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) ~~~ createElm的作用是通过**虚拟节点创建真实的DOM并插入到它的父节点**。 最后调用insert方法,把DOM插入到父节点中,因为是递归调用,子元素会优先调用insert,所以真个vnode树节点的插入顺序是先子后父。 insert其实就是调用原生DOM的API进行DOM操作。 ![](https://box.kancloud.cn/9286e387ffaf5851147f3ba171d7549a_1083x510.png) 实际项目中,我们是把页面拆成很多组件的,**Vue另一个核心思想就是组件化**。 *****