Vue源码一行行分析(二)双向绑定原理

mac2024-05-14  26

一、前言

今天来一步步实现Vue双向绑定,先引入一张的图片,此图片来源,他这篇文章写得挺好的,大家可以去看看,而我这篇文章的意义在源码的分析以及自身的一些理解,不足之处,请指正,谢谢。可能就光看图不是很理解,那么从源码的角度来解析图,在回过头来看这张图,那么你会理解很多,活不多说,开始吧。

二、源码分析

接我自己上篇Vue源码一行行分析(一)整体分析,没看的先看我的第一篇吧,可以得知,在initState方法中进行数据监控绑定。

function initState (vm) { vm._watchers = []; var opts = vm.$options; if (opts.props) { initProps(vm, opts.props); } // 先检查options里有没有props 初始化props相关 //先检查options里有没有methods 初始化methods相关 因为我们现在讲的是双向绑定,这里的先不讲,后续补充 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); } // 初始化computed if (opts.watch && opts.watch !== nativeWatch) { // 初始化监听 initWatch(vm, opts.watch); } } function initData (vm) { var data = vm.$options.data; // 从vm.$options中获取data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {};// 如果data是方法 执行getData,getData方法内部就是执行data.call 调用获取方法返回值 if (!isPlainObject(data)) { //判断data是不是纯粹的对象 不是的报下面的警告 要求data函数应该返回一个对象 data = {}; 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 var keys = Object.keys(data); // 获得data对象的键 var props = vm.$options.props; // 获得options里面的props var methods = vm.$options.methods;// 获得options里面的methods var i = keys.length; while (i--) { // 循环遍历 var key = keys[i]; { if (methods && hasOwn(methods, key)) { // 使用Object.prototype.hasOwnProperty来判断在data中定义的键是否也在methods中定义了 // 这也就是为什么我们在data中定义了一个变量,在methods定义了一个同名字变量报错的原因, // 下面的props同理 ,所以data,methods props中不能有相同名字的变量,因为不管是方法 // 还是props还是data都会放到Vue的实例上,如果相同名字的话 我不知道你要取的是data // 还是 methods,还是porps warn( ("Method \"" + key + "\" has already been defined as a data property."), vm ); } } if (props && hasOwn(props, key)) { warn( "The data property \"" + key + "\" is already declared as a prop. " + "Use prop default value instead.", vm ); } else if (!isReserved(key)) { //如果data的键不是以$或者_开头的话 拦截代理 proxy(vm, "_data", key); // 因为 } } // observe data observe(data, true /* asRootData */); }

因为data中的值放置在_data中,所以你要获得msg的值你只能通过this._data.msg获得,那我岂不是获取每一个值都要this._data.xxx,对于程序员来说这是难以接受的,我们只想要什么取什么 this.xxx才是我想要的,怎么简单怎么来,所以才有proxy方法,使用Object.defineProperty

var sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }; function proxy (target, sourceKey, key) { // target代表vm,sourceKey代表_data,key代表你要取的值 // 意思就是你如果要使用this.msg获得msg,其实就是指向this._data.msg,得到同样的效果, // 所以该方法名叫代理,使用this.msg代理this._data.msg,简单方便 sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] }; sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val; }; Object.defineProperty(target, key, sharedPropertyDefinition); }

接下来到最后的observe方法了

function observe (value, asRootData) {// 将data传入,第二参数是否作为根节点数据 if (!isObject(value) || value instanceof VNode) { // 如果传入的data不是对象 或者不输入VNdode 虚拟节点 则返回 该判断用于递归结束判断,后面会讲到 return } var ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { // 一系列的判断 就开始最上面图讲的Observer观察者 ob = new Observer(value); } if (asRootData && ob) { ob.vmCount++; } return ob } var Observer = function Observer (value) { this.value = value; // 将data放入到Observer this上的value上 this.dep = new Dep(); // 最上图的Dep 消息管理中心 this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { // 如果传入的data是数组 if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { // 传入不是数组 this.walk(value); } }; var Dep = function Dep () { this.id = uid++; this.subs = []; // 定义一个订阅者列表 }; Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); // 获得data的键 开始循环监听 for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i]); } }; function defineReactive$$1 ( obj, // data key, // data中的某一个键 val, customSetter, shallow ) { var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); // 获得data中的key的修饰符 if (property && property.configurable === false) { //如果该键不能写 返回 return } // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; if ((!getter || setter) && arguments.length === 2) { // 如果传入的参数只有两个,代表是数据监听 val = obj[key]; } var childOb = !shallow && observe(val); // 递归,这里也上面的observe首先判断对应,如果val还是对象 //则递归,如果不是对象了 结束递归 // 下方是Vue的双向绑定核心 使用Object.defineProperty数据劫持 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { // 当取值的时候触发 例如要获得msg 使用this.msg就会进入此方法 var value = getter ? getter.call(obj) : val; //如果该属性自身带有getter使用自身,否则取得原值 if (Dep.target) { //这里就比较复杂了 涉及到Watcher // 如果有新的Watcher的话,Watcher要告诉消息管理器去添加订阅者 dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val; /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { // 如果新值与旧值相等 不改变 直接返回 return } /* eslint-enable no-self-compare */ if (customSetter) { customSetter(); } // #7981: for accessor properties without setter if (getter && !setter) { return } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); // 对设置的值进行observe劫持 dep.notify(); // 消息管理器进行通知 } }); }

下面三段代码是取值的时候 添加订阅者

Dep.prototype.depend = function depend () { if (Dep.target) { // 这里Dep.target就是Watcher 当有新的Watcher的时候,就放入到Dep.target中 Dep.target.addDep(this); } }; Watcher.prototype.addDep = function addDep (dep) { var id = dep.id; // 通过Dep的id来判断订阅者中是否已经有相同的订阅者了,没有则添加,有则不添加 if (!this.newDepIds.has(id)) { this.newDepIds.add(id); this.newDeps.push(dep); if (!this.depIds.has(id)) { dep.addSub(this); } } }; Dep.prototype.addSub = function addSub (sub) { this.subs.push(sub); // 添加到Dep的subs数组列表中 };

下面的代码段是设置值得时候通知Watcher发生更新

Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice(); if (!config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort(function (a, b) { return a.id - b.id; }); } for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } }; Watcher.prototype.update = function update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); } }; Watcher.prototype.run = function run () { if (this.active) { var value = this.get(); if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value var oldValue = this.value; this.value = value; if (this.user) { try { this.cb.call(this.vm, value, oldValue); } catch (e) { handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\"")); } } else { this.cb.call(this.vm, value, oldValue); } } } };

相信你现在已经有点晕了,也有可能是我讲的不是很清楚,因为封装很多方法,总跳,所以就很繁琐,你不从整体看的话,跳的方法过多,就有点难了,我现在画个流程图捋捋思路。 通过上面 其实,双向绑定原理主要分四个模块,分别为Observer(观察者),Dep(消息管理中心),Watcher(监听者), Compile(编译者),首先使用Observer 采用Object.defineProperty进行数据劫持,在使用发布订阅设计模式,进行监听,当取值的时候,通知消息管理中心添加监听者,当设置值得时候,Watcher通知改变,Compile重新解析编译,更新相应视图,而将vue的模板比如什么v-model ,{{}}转换成html,就要通过Compile编译解析,所以这就是双向绑定的基本原理,这里没有过多的讲述Compile,因为现在还在学习,Compile是通过模板转换成AST虚拟树,在转换成VNode,最后解析成html的过程,还有很多难点,后续再更新吧

最新回复(0)