Vue 是数据驱动视图实现双向绑定的一种前端框架,采用的是非入侵性的响应式系统,不需要采用新的语法(扩展语法或者新的数据结构)实现对象 (model) 和视图(view)的自动更新,数据层(Model)仅仅是普通的 Javascript 对象,当 Modle 更新后 view 层自动完成更新,同理 view 层修改会导致 model 层数据更新。
双向绑定实现机制
Vue 的双向绑定实现机制核心:
- 依赖于
Object.defineProperty()
实现数据劫持 - 订阅模式
Object.defineProperty()
MDN: Object.defineProperty () 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
我们使用 Object.defineProperty ()
进行简单的数据劫持操作:
var Book = {
name:"jsBook"
};
Object.defineProperty(Book, 'name', {
enumerable: true,
configurable: false,
set: function (value) {
this._name = `${value || "JavaScript编程思想"} `;
},
get:function() {
return `《${this._name}》`;
},
});
Book.name = null;
console.log(Book.name) // 《JavaScript编程思想》
这只是对于一个属性的设置,当我们需要劫持对象的所有的属性的时候,可以封装 Object.defineProperty ()
方法并借用 Object.keys ()
进行对象可枚举属性的遍历:
var Person = {
name:"smith",
skill:"熟练使用Java"
};
let myReactive = function(obj, key , val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
set: function (value) {
val = value;
},
get:function() {
return val;
},
});
}
Object.keys(Person).forEach(key => {
myReactive(Person, key, Person[key])
})
Person.skill = "熟练使用JavaScript";
console.log(Person.name + Person.skill); // smith熟练使用JavaScript
通过简单的例子我们可以了解 Object.defineProperty ()
的基本数据劫持操作,这也是 Vue 的响应式实现的基本原理,Vue 在初始化对象的之前将数据定义在 data 对象中,初始化实例时对属性执行 getter/setter 转化过程,所以只有定义在 data 对象上的属性才能被劫持(被转化),同时因为 JavaScript 的限制 Vue 不能检测对象属性的添加和删除。
- 节选 染陌同学:Vue 响应原理
function observe(value, cb) {
Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb))
}
class Vue {
constructor(options) {
this._data = options.data;
observe(this._data, options.render)
}
}
let app = new Vue({
el: '#app',
data: {
text: 'text',
text2: 'text2'
},
render(){
console.log("render");
}
})
Vue 源码分析
- 初始化 Vue 实例
/*initMixin就做了一件事情,在Vue的原型上增加_init方法,
* 构造Vue实例的时候会调用这个_init方法来初始化Vue实例
*/
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this;
vm._self = vm
/*初始化生命周期*/
initLifecycle(vm)
/*初始化事件*/
initEvents(vm)
/*初始化render*/
initRender(vm)
/*调用beforeCreate钩子函数并且触发beforeCreate钩子事件*/
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
/*初始化props、methods、data、computed与watch*/
initState(vm)
initProvide(vm) // resolve provide after data/props
/*调用created钩子函数并且触发created钩子事件*/
callHook(vm, 'created')
}
}
- 初始化状态
我们可以了解 initState (vm) 方法用来初始化 Vue 我们配置的方法,数据等状态,所以我们重点研究一下 initState () 方法:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
/*初始化props*/
if (opts.props) initProps(vm, opts.props)
/*初始化方法*/
if (opts.methods) initMethods(vm, opts.methods)
/*初始化data*/
if (opts.data) {
initData(vm)
} else {
/*该组件没有data的时候绑定一个空对象*/
observe(vm._data = {}, true /* asRootData */)
}
/*初始化computed*/
if (opts.computed) initComputed(vm, opts.computed)
/*初始化watchers*/
if (opts.watch) initWatch(vm, opts.watch)
}
- 初始化数据
在初始化数据的时候,我们需要判断 data 中的 key 不能与 props 定义过的 key 重复,如果冲突将会以 props 定义的 key 优先,并且告警提示冲突。
/*初始化data*/
function initData (vm: Component) {
/*得到data数据*/
let data = vm.$options.data
/*遍历data对象*/
const keys = Object.keys(data)
const props = vm.$options.props
let i = keys.length
//遍历data中的数据
while (i--) {
if (props && hasOwn(props, keys[i])) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${keys[i]}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
}
}
// observe data
/*通过observe实例化Observe对象,开始对数据进行绑定
* asRootData用来根数据,用来计算实例化根数据的个数
* 下面会进行递归observe进行对深层对象的绑定。则asRootData为非true
*/
observe(data, true /* asRootData */)
}
- 观察对象
export class Observer {
constructor (value: any) {
if (!Array.isArray(value)) {
/*如果是对象则直接walk进行绑定*/
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
/*walk方法会遍历对象的每一个属性进行defineReactive绑定*/
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
订阅发布
- 创建 Wahtcher
Vue 对象 init 后会进入 mount 阶段,执行 mountComponent 函数:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 如果没有!vm.$options.render方法,就创建一个空的VNODE,不是生产环境啥的报错
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
// 报错的代码
}
// 调用一下回调函数
callHook(vm, 'beforeMount')
// 定义一个updateComponent方法
let updateComponent
// 如果啥啥啥条件,那么updateComponent定义成如下方式,否则直接调用_update方法
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
// 在这里的核心调用来一下_render方法创建来一个vnode
const vnode = vm._render()
vm._update(vnode, hydrating)
}
} else {
// 这里是定义的updateComponent是直接调用_update方法
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 实例化一个渲染watcher,用处是初始化的时候会执行回调函数,
// 另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// 函数最后判断为根节点的时候设置 vm._isMounted 为 true, 表示这个实例已经挂载了,
// 同时执行 mounted 钩子函数
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
- Wather 构造函数
在 mountComponent 函数内部,通过 new Wather () 创建监听器 ,Vue Component 都会经过一次 mount 阶段并创建一个 Wather 与之对应。
Wather 的构造函数 new Watcher(vm, updateComponent):
- vm :与 Wather 对应的 Vue Component 实例,这种对应关系通过 Wather 去管理
- updateComponent:可以理解成 Vue Component 的更新函数,调用实例 render 和 update 两个方法,render 作用是将 Vue 对象渲染成虚拟 DOM,update 是通过虚拟 DOM 创建或者更新真实 DOM
总结
- Vue Component 都有一个对应的 Wather 实例
- Vue Component 实例初始化的时候通过 data 绑定对象,data 上的属性通过 getter/setter 转化
- Vue Component 执行 render 方法的时候,data 定义的数据对象会被读取执行 getter 方法,Vue Component 会记录自己依赖的 data
- 当 data 数据被修改的时候,通过 setter 方法更新数据,Wather 会通知所有依赖此 data 的组件去调用 Vue Component 的 render 函数更新视图。
必须 注册 为本站用户, 登录 后才可以发表评论!