数据驱动
1 )概述
-
vue的一个核心思想,就是数据驱动
-
所谓数据驱动,就是指视图是由数据驱动生成的
-
对视图的修改并不会直接操作dom,而是通过修改数据
-
它大大简化了代码量,特别是当交互复杂的时候,只关心数据的修改会让代码的逻辑变得非常清晰
-
因为dom变成了数据的映射,我们所有的逻辑都是对数据的修改,而并不关注dom,这样的代码非常有利于维护
-
在vue.js中,可以采用简洁的模板语法来声明式的将数据渲染为 dom,示例
// main.js import Vue from 'vue' var app = new Vue({ el: '#app', /* data: { message: 'Hello' }, */ data() { return { message: 'Hello' } } }) // App.vue <div id="app"> {{ message }} </div>
- 这实例的配置有两个参数
- el: 是它的一个挂载的dom对象
- data: 是相关的数据
- 在模板中就声明是起了这样的一个差值message
- 最终映射到浏览器上,可以看到这个文本节点
- 它实际上就生成了一个字符串文本
- 这实例的配置有两个参数
-
这个例子是为了说明这个数据在js中定义的这个数据 最终是怎么渲染到dom上的
-
这就是 new Vue 的时候帮我们做的这些事情
2 )数据驱动的关注
- 第一,分析数据是怎么映射到dom的
- 传入了这样一个javascript对象,最终怎么生成到dom上的
- 第二,数据的变化驱动视图的变化
- 对message的修改,视图是怎么跟着变化的
new Vue时,做了哪些处理
- 在 src/core/instance/index.js 中
function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) export default Vue
- 它实际上就是一个class,也就是一个function实现的这个class
- 它实际上就是执行了这个原型上的 _init 方法
- 这个 _init 方法在 src/core/instance/init.js 中定义
- 注意,这个 _init 方法是执行下面的
initMixin(Vue)
时,才被挂载的 - 就在这个 initMixin 方法的内部,挂载了 _init, 如下
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) } }
- 在这个 _init 方法中,做了一堆初始化的工作
- 比如定义 uid
- 合并options
- 它会把传入的options,最终都merge到 $options 上
- 可以通过 $options.el 访问到我们初始化时的 el 对象
- 可以通过 $options.data 访问到我们定义的 data
- 后面就是一堆初始化函数
- 比如,初始化生命周期,事件中心,render,injections, state, provide, state 这些
- 注意,中间的两次 callHooks 的调用
- 最后,挂载这个 el 对象
- 在上面的demo里面, 定义了这个data,需要看下这个data怎么初始化的?
var app = new Vue({ el: '#app', /* data: { message: 'Hello' }, */ data() { return { message: 'Hello' } } mounted() { console.log(this.message); // Hello console.log(this._data.message); // Hello } })
- 现在看下,为何能够通过
this.message
访问到数据的 - 进入上述的
initState
, 定义在 src/core/instance/state.jsconst sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop } export function proxy (target: Object, sourceKey: string, key: string) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] } sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val } Object.defineProperty(target, key, sharedPropertyDefinition) } export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } } function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} if (!isPlainObject(data)) { data = {} process.env.NODE_ENV !== 'production' && warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ) } // proxy data on instance const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] if (process.env.NODE_ENV !== 'production') { if (methods && hasOwn(methods, key)) { warn( `Method "${key}" has already been defined as a data property.`, vm ) } } if (props && hasOwn(props, key)) { process.env.NODE_ENV !== 'production' && warn( `The data property "${key}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else if (!isReserved(key)) { proxy(vm, `_data`, key) } } // observe data observe(data, true /* asRootData */) } export function getData (data: Function, vm: Component): any { // #7573 disable dep collection when invoking data getters pushTarget() try { return data.call(vm, vm) } catch (e) { handleError(e, vm, `data()`) return {} } finally { popTarget() } }
- 在
initState
中- 如果有 props, 则初始化 props
- 如果有 methods, 则初始化 methods
- 如果有 data, 则初始化 data,看 initData
- 在
initData
中- 从 $options 中拿到 data
- 判断 data 是否是一个 function, 通常我们推荐写一个function, 而非直接使用一个对象
- 如果是函数,则调用
getData
, 否则直接使用 data - 后面处理好的 data 不是对象,在 dev 环境中进行输出警告
- 再后面 拿到 keys, props, methods,它们之间就做了一个循环对比
- 比如说在这个 data 里面定义了这个message
- 那就不能在 props 下也用 message
- 或者在 methods 里面用这个 message
- 为什么不能用,为什么会冲突,是因为它们最终都会挂载到 vm 上
- 也就是说, 这个message,它最终会挂到这个this
- 这样,this.message 就可以访问到当前数据了
- 那这个是怎么实现的呢?实际上就通过这个 proxy 函数实现的
- proxy 顾名思义就是代理
- 它实际上通过这个
sharedPropertyDefinition
对象定义了一个get 和一个set 两个函数 - 然后通过 Object.defineProperty这个方法,代理了这个 target 的 key
- 就是对 target 的 key 的访问做了一层 get 和 set
- 这个 target 实际上就是 vm,也就是说访问
vm[key]
, 这个 getter 函数就会执行,返回this[sourceKey][key]
- 这个 sourceKey 就是 _data, 也就是说,当我们访问 this.message 实际上是从 this._data.message 中获取的
- 它实际上通过这个
- 因为它是通过这个 proxy 做层代理,在调用 proxy 的时候,实际上就是把这个 _data 作为 sourceKey 传入
- 这是通过 this.message 能狗访问到 message 定义到的数据的原因,但要注意,_data在后续开发中不要使用,这个
_
表示私有,不对外提供 - 类似的,props的访问也类似,这里暂时跳过
- 之后通过调用 observe 对 data 做一个响应式处理, 这块也跳过
- 在
- 总结
- 当执行 new Vue 的时候,它实际上是执行了 _init 的方法,这个方法做一堆初始化的工作
- 先是对 options做合并,接下来就执行一系列的方法
- 在其中的 initState 过程中,对 data 做一层 proxy 的处理,最后对data做响应式处理
- 最后会调用 vm.$mount 将 el 进行挂载
扩展
调试Vue2源码小技巧
- 一般而言,我们都在 dev 环境下调试源码的,没有说在生产环境下调试
- 我们如果需要调试哪个API或相关流程,需要先建一个小demo项目,之后在当前项目的 node_modules 中
- 找到 vue 目录,查看 package.json 找到 module 配置,
dist/vue.runtime.esm.js
- 其实这里,并不是真实的入口 !!!
- 这个demo实际上是 vue-cli 生成的,这个模板工程是由webpack构建出来的,在build/webpack.base.conf.js 中的
- resolve 属性中,配置了 alias
{ 'vue': resolve('node_modules/vue/dist/vue.esm.js'), '@': resolve('src'), }
- 所以,真实的源码是在 node_modules/vue/dist/vue.esm.js 中
- 这个在 vue-cli 初始化工程的时候,如果选择了 Runtime + Compiler 版本时,会添加这个 alias 的 vue 这行代码
- 所以,真正使用的是这个文件,这个是一个大的打包后的代码
- 可以在这个文件中搜索方法,并进行 debugger
- 后续如何调试,就忽略不再赘述了