都知道vue中实现了数据和视图的双向绑定,但具体是如何实现的呢?
今天就说说 我阅读的vue2中的代码实现的个人所得,如果说错了,欢迎指正。
注:我阅读的vue2代码的版本是v2.2.6,且都是以单文件的方式展示列子, 可以结合下一篇续给出的实际例子结合起来看,更容易理解
话不多说,首先上结论,然后再通过图以及代码进行详细解释
结论:m->v: 利用观察者模式,当数据改变的时候通知其观察者进行更新,
v->m: 利用事件绑定对数据进行设值
实现过程:通过Object.defineProperty的方法定义 get和set方法对数据的操作拥有代理权,并且每个属性都会有一个对应的依赖,
每个vm组件都会有一个或多个watcher,当获取vm或者data的数据的时候就会调用get方法,如果这个时候有watcher正在运行观察,
那么就把这个依赖和此watcher都加入到各自的对象下的相关队列中去
1)model到view:当设置vm或者data对象的数据的时候,就会调用set方法,
如果这次结果 和上次的value不同,就让此依赖中的所有watcher加入到scheduler(调度者)的更新队列中去,
等待nextTick更新scheduler的队列,调用watcher的update方法,然后运行getter方法比较value值和之前保存的value值是否相同,
如果不同,则调用cb(回调函数),对应vm来说getter是vm.update(vm.render()),当调用getter的时候,整个过程就完成了model到view的更新了
2:view到model:一般用v-model方法进行实现,默认的是绑定dom组件的input事件(这个看源码的实现,如果你设置option.model.event的值的话,
事件就会变成你设置的值),当触发此事件的时候,会调用你绑定的值进行设置,就又回到1)过程了,当然也可以自己绑定自己关心的事件实现设值的效果
上图:
可以从结论中看到,其中有3个比较重要的部分,data对应的dep和vm对应的watcher以及把它们进行关联的Object.defineProperty定义的get和set方法,
而实现这一切的基础就是调用Object.defineProperty对属性设置get和set代理方法,所以vue2只能支持ie8以上,因为ie8及一下没有这个方法,也没法用shim的方式来实现
详细解释
一. watcher
export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; lazy: boolean; sync: boolean; dirty: boolean; active: boolean; deps: Array; newDeps: Array ; depIds: Set; newDepIds: Set; getter: Function; value: any;
constructor (vm: Component, expOrFn: string | Function, cb: Function, options?: Object) {}
run(){} get(){} update(){} addDep(){} cleanupDeps{} evaluate(){} teardown(){} }
注:为了避免太长不方便看,方法的具体实现就没有帖出来了,如果有兴趣的可以去源码看看
简称观察者,通常意义的解释,就是对某些事物感兴趣而去观察他的人,用在vue2里面就是对一些表达式结果感兴趣而进行观察的对象,
用来统计更新vm的updateComponent方法,computed属性或者watch属性中的expOrFn(表达式或者方法)
watcher对象有3个比较重要的属性:
getter:关心的表达式或者方法
value: 运行getter的结果,在new watcher的时候如果lazy为false就会调用getter一次来初始化这个值
cb:当运行getter方法得到的value值和之前的value值不同的时候,会调用cb,对于vm组件来说就是更新组件
还有一些其他属性:
id: watcher的唯一标示,当对一个周期内的所有watcher执行更新的时候,会先根据这个id 从小到大进行排序,而且父级的vm的对应的id总是小于子集
lazy:是否懒更新, 也就是当它关注的表达式中有相关数据改变的时候,它才更新,不然就一直用value值,像computed中的watcher的lazy就是true
dirty:是否已经脏了,如果脏了,当获取这个值的时候就重新调用getter获得新值,否则直接返回value的值,和lazy有关系
user: cb回调函数是否是用户传进来的,如果是的话执行这个回调就会用try catch的方式,以避免出错
active: 这个watcher是否是活动的,如果是活动才会去运行getter
deps:对应的所有依赖,是一个数组
newDeps:这次更新后,收集到的下一轮更新所相关的依赖
depIds:所有依赖的id,是一个set对象
newDepIds: 下一轮的依赖id所对应的set对象
注:这里面的deps的作用个人觉得是:当vm销毁的时候或者运行了一此get方法的时候,从相关的依赖中删除自己,当某些需要的时候,重新和依赖关系,
目前只看到computed的watcher 在获取值的时候,如果watcher的dirty为true,就会重新和deps里面的依赖重新建立关系,其他地方就没看到有使用了- -!
先上一个例子,下面会用到:
{
{infoName}}{
{infoSex}}
注: 上述代码是为了展示 watcher而强行拼凑的代码,所以有重复的地方,不要在意这些细节
有3个地方会有对应的watcher
1.一个vm组件会对应一个watcher,这个watcher关注的是vm的_render方法,每个vue文件的template里面的内容都会被编译成一个_render方法,
并且里面用到的属性或者computed都会转化成_vm.name 或者_vm.infoName 的代理形式,具体的转化过程在dep里面说到
2.computed对象里面的每个属性,都有一个对应的watcher,如上例:infoName,infoSex都会有一个对应的watcher,并且生成的watcher的lazy为true,
即它们都是懒更新的,只有里面用到的相关数据出现变化的时候,这个watcher才会执行getter方法
3.watch对象里面的每个属性,都有一个对应的watcher,如上例:sex,name,"resultObj.allInfo"都会有一个对应的watcher
"resultObj.allInfo" 这个对应的watcher需要特殊说明下它的getter,先看代码
if (typeof expOrFn === 'function') { this.getter = expOrFn; } else { this.getter = parsePath(expOrFn); if (!this.getter) { this.getter = function () {};}var bailRE = /[^\w.$]/;function parsePath (path) { if (bailRE.test(path)) { return } var segments = path.split('.'); return function (obj) { for (var i = 0; i < segments.length; i++) { if (!obj) { return } obj = obj[segments[i]]; } return obj }}/// function get(){ if (this.user) { try { value = this.getter.call(vm, vm); } catch (e) { handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\"")); } } else { value = this.getter.call(vm, vm); } }
如果初始化watcher的时候传的expOrFn 是function,那么这个getter就直接是这个function,否则就就行转化,运行parsePath方法得到一个function
当然这个function有一个判断,是路径形式的才行,当运行getter方法的时候,传入vm作为obj,那么其实resultObj.allInfo 得到的是 vm.resultObj.allInfo的值,
当然 不可能直接vm["resultObj.allInfo"]或者vm."resultObj.allInfo"这样得到,所以用了一个循环来实现
这3种对应的watcher创建的时候分别为:
1) 使用$mount方法的时候
Vue$3.prototype.$mount = function (el, hydrating) { el = el && inBrowser ? query(el) : undefined; return mountComponent(this, el, hydrating)};function mountComponent (vm,el,hydrating) { var updateComponent = function () { vm._update(vm._render(), hydrating); }; vm._watcher = new Watcher(vm, updateComponent, noop);}
注:1.有些地方我是从分模块实现的源码中拷贝过来的,有些是直接是为了方便查找,就直接在vue.common.js中拷贝过来,所以可能代码风格有些不一样,
而且为了更方便理解,会对某些地方做小小的改动,会删除一些和本话题无关的代码实现过程
2.hydrating 这个属性和vdom(虚拟dom)实现有关系,在这里暂时不去解释它的作用, 单词意思为:保湿
通过实现可以看出来,watcher的getter 就是vm的render方法(update是更新组件,和数据没有关系,所以把getter转化为render),每个vm都有自己独立对应的一个watcher,
这个watcher关心的是整个组件的渲染
上面说的是 在使用$mount方法的时候 会创建这个watcher,但是有时候,我们在创建组件的时候,没有使用$mount ,比如
const app = new Vue({ el:"#app" router: router, store:store, render: h => h(App)});
这个时候就没有用$mount,那么它就不会有watcher了吗?
先上代码:
function Vue (options) { this._init(options)}Vue.prototype._init = function (options?: Object) { //...其他初始化实现 if (vm.$options.el) { vm.$mount(vm.$options.el) } }
注: 我吧_init方法里面不相关的代码都删除了,只留下了我们关心的地方
在new vue的时候,我们传入的对象有个el属性,关键就在这里,而在vm初始化的时候会判断传进来的对象有没有这个属性,如果有就会自动去调用$mount方法,而不需要手动
当然如果又没有el,又没有手动调用$mount方法,那么这个组件就不会出现在view上,也不会创建对应的watcher
2.解析computed属性的时候
Vue.prototype._init = function (options?: Object) { initState(vm)}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) initWatch(vm, opts.watch)}function initComputed (vm: Component, computed: Object) { const watchers = vm._computedWatchers = Object.create(null) for (const key in computed) { const userDef = computed[key] let getter = typeof userDef === 'function' ? userDef : userDef.get // create internal watcher for the computed property. watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions) }}
注:1.把有些和此话题不相关的代码都删除了,所以帖出来的代码不是完整的
2.在逛vue论坛的时候,遇到有人提问:在prop属性设置default为methods里面的方法为什么报空,大家看看initState的初始化顺序就明白了
3.data,computed,watch等都会存储在vm.$option中
在解析computed属性的时候,会对里面的每个属性都会有创建一个对应的watcher,并且会用用这个属性的key作为_computedWatchers 对象的key来保存这个watcher
当你默认传一个function的时候,会把这个这个function当做watcher的getter,否则就会认为你传了一个包含了get属性的对象,并且是一个方法,
这个时候就会用这个get方法当做watcher的getter
3.使用$watch方法的时候
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 { createWatcher(vm, key, handler) } }}function createWatcher (vm: Component, key: string, handler: any) { let options if (isPlainObject(handler)) { options = handler handler = handler.handler } if (typeof handler === 'string') { handler = vm[handler] } vm.$watch(key, handler, options)}Vue.prototype.$watch = function ( expOrFn: string | Function, cb: Function, options?: Object ): Function { const vm: Component = this options = options || {} options.user = true const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { cb.call(vm, watcher.value) } return function unwatchFn () { watcher.teardown() } }
和使用$mount方法一样,无论是你初始化的对象包含了watch属性或者使用$watch 都会创建相对于的watcher
使用$watch会返回一个方法,当你对观察的expOrFn不在感兴趣的时候,可以用来删除这个观察者
二.dep
export default class Dep { static target: ?Watcher; id: number; subs: Array; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }}
简称依赖,对vm中data返回的每个属性都会初始化一个依赖,用来收集对这个属性感兴趣的watcher,也就是watcher中运行getter方法会用到的相关属性,
那么这个属性对应的依赖就会把watcher加入到dep下的subs队列中去,也会把本身dep加入到watcher中的deps队列中去,
当这个属性的值改变的时候,通知watcher进行更新操作,也就是执行getter方法
属性解释:
subs:对某个属性感兴趣的watcher队列
dep一般对应vm的中data方法返回的对象以及它的属性
export default { data(){ return { name:"", sex:1, items:[] } }}
比如上面的 data会有一个dep,name和sex也分别有一个dep
dep的创建过程
function initState (vm) { if (opts.data) { initData(vm); } else { observe(vm._data = {}, true /* asRootData */); }}function initData (vm) { var data = vm.$options.data; data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}; if (!isPlainObject(data)) { data = {}; } var keys = Object.keys(data); var props = vm.$options.props; var i = keys.length; while (i--) { if (props && hasOwn(props, keys[i])) { ///warn } else if (!isReserved(keys[i])) { proxy(vm, "_data", keys[i]); } } // observe data observe(data, true /* asRootData */);}function observe (value, asRootData) { if (!isObject(value)) { return } var ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } else if ( observerState.shouldConvert && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value); } return ob}var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { var augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); this.observeArray(value); } else { this.walk(value); }};Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i], obj[keys[i]]); }};function defineReactive$$1 ( obj, key, val, customSetter) { var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return } // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; var childOb = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; if (Dep.target) { 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 (process.env.NODE_ENV !== 'production' && customSetter) { customSetter(); } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = observe(newVal); dep.notify(); } });}Observer.prototype.observeArray = function observeArray (items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); }};
注:vm初始化会调用initState方法,所以省略了init的过程
整个过程简而言之就是
1).initData
在初始化data的时候,是用一个三元运算符,判断data是否是一个function,如果是的话,就运行 getData方法
function getData (data, vm) { try { return data.call(vm) } catch (e) { handleError(e, vm, "data()"); return {} }}
其实getData 也就是运行一次data方法而已,只是为了安全考虑,加了try catch
如果不是的话,就返回 data || {} 给它,这里有个小技巧就是,如果data不为空就返回data,否则就返回一个{},
然后 在对data里面的属性设置代理,让我们能够用vm.key的方式来获取或者设置data里面的数据,而不用vm._data.key的方式
在方法的最后面 就是调用observe 来生成一个observer(可以依赖的对象)
2).observe
首先判断 是不是一个object,当然 这里的object 是一个泛型,是指 typeof 为 "object" 且 != null 的对象,也可以是Array,
function isObject (obj) { return obj !== null && typeof obj === 'object'}
如果不是一个object 那么就会中断
再判断 是否已经包含了一个 observer 对象,如果有 则返回这个observer对象
再判断
observerState.shouldConvert: 是可以转换的
!isServerRendering(): 不是服务器渲染
(Array.isArray(value) || isPlainObject(value)) 是 数组 或则 Object 类型
Object.isExtensible(value): 这个对象是可以添加属性的, 做这个判断是因为 会在Object上 新增一个__ob__属性来对应它的Observer对象 !value._isVue: 不能是 vm对象, vm组件 严格来说 也是Object对象,但是他不能被观察,所以有这个判断
注: 判断还挺多的。。。。
如果上述条件 任何一个不满足 就会中断,不过我们只需要关心的是。。我们传进来的数据满足是 数组 或者object对象就行了,其他的一般不会触及
好了 下一步就是肉戏来了,就是创建一个 observer 对象
3). new observer
保存value数据对象,然后最重要的相关核心实现就从这里开始了,
首先创建一个dep依赖(说了这么多终于说到它了),这个dep 就对应的这个value数据对象,
然后 会判断这个value是不是数组对象,
如果是的话, 先执行
augment(value, arrayMethods, arrayKeys) 而 augment 一般会对应这个方法
function protoAugment (target, src) {/* eslint-disable no-proto */target.__proto__ = src;/* eslint-enable no-proto */}
这一步比较关键,把这个数组对象默认的prototype对应的方法替换成arrayMethods 里面的方法,而arrayMethods 这个对象里面包含了哪些方法呢,请看代码
var arrayProto = Array.prototype;var arrayMethods = Object.create(arrayProto);[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) { // cache original method var original = arrayProto[method]; def(arrayMethods, method, function mutator () { var arguments$1 = arguments; // avoid leaking arguments: // http://jsperf.com/closure-with-arguments var i = arguments.length; var args = new Array(i); while (i--) { args[i] = arguments$1[i]; } var result = original.apply(this, args); var ob = this.__ob__; var inserted; switch (method) { case 'push': inserted = args; break case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); break } if (inserted) { ob.observeArray(inserted); } // notify change ob.dep.notify(); return result });});
从代码看:包含了 'push','pop','shift','unshift','splice','sort','reverse' 方法,并且arrayMethods的prototype是Array的prototype,
具体作用在下面回和例子结合说到
然后,再执行 observeArray,
Observer.prototype.observeArray = function observeArray (items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); }};
其实也就是对数据里面的每个元素再进行观察, 从observe的判断条件来看,普通的数据就不要想了,此元素 必须是一个object 或者array,才会生成一个observer对象(这点很重要)
好了,通过上面对数组的实现,可以解释一下 我们平时 在用数组稍不注意就会犯的一个错误, 就是改变一个数组的普通元素的值,但是 界面上却没有响应它的改动呢,
比如
{
{value}}
上述代码,会在两秒之后改变界面的显示吗?
不会,因为 this.ary[2] = 10, 这个只是普通的操作,vue2 没有对这个属性添加相关的dep(我觉得是为了性能考虑吧,不然的话 数组里面的数据大了,对每个数据都建立一个dep,那就太恐怖了)
这样的操作,是没有任何改变的,
而上面vue2 特意自己实现了array 的一些相关方法去实现这个效果,所以如果要改变普通元素的数据的时候我们可以这样,
mounted(){ setTimeout(()=>{ this.ary.splice(2,1,10); }, 2000); }
感兴趣的可以测试下,
起作用的原因在于arrayMethods这个对象,当我们操作splice的时候,不是操作的ary的原生的splice方法,而是操作的arrayMethods里面的splice方法,
而这个方法在执行了原生的对应的方法后,还会去调用ob.dep.notify()方法执行更新
以上是如果这个数据类型是数组的时候,当不是的时候,那么执行walk方法,
然后获取对象的keys 循环对key调用defineReactive方法,然后当这个数据 又是一个object或者array对象的时候,又再一次去观察它
可以说defineReactive 这个方法里面的实现,是实现双向绑定最最核心的一个方法了,其他的一系列都是在这基础上做扩展,或是为了健壮性或者为了解耦等等,
这个方法实现了对data的每个属性创建一个dep,并且对data的原生属性的操作利用Object.defineProperty方法定义一个set和get的代理,让data和dep产生关系,
而且 在set和get具体实现里,也实现了 让dep和watcher 产生关系,所以说这个是vue2实现双向绑定最最最核心的方法了,因为这么重要,说就单独在下一个部分来解释它
三 dep 和 watcher 是如何联系起来的
function defineReactive$$1 ( obj, key, val, customSetter) { var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return } // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; var childOb = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; if (Dep.target) { 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 (process.env.NODE_ENV !== 'production' && customSetter) { customSetter(); } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = observe(newVal); dep.notify(); } });}
下面就来看看 这个方法的具体实现:
首先定义了一个dep,然后在获取这个属性的描述对象,用这个对象来判断这个属性是不是可以配置的,如果为否,则退出,因为使用Object.defineProperty
这个方法是需要这个属性是可以配置的
然后获取这个属性的 get和set方法并缓存,因为Object.defineProperty会重定义这两个方法,会覆盖它们,缓存起来以便于调用
再然后 let childOb = observe(val) 这句话,就实现生成子对象的observer,这样就实现了循环调用,对所有子对象都能够生成observer以及子对象的属性值的dep
当然,这个子对象得是 Object 或者 array
请注意:核心来了,使用Object.defineProperty方法 定义set 和get方法
我们看set方法实现:
首先得到value值,这里用三元运算符来判断之前缓存的get方法是否存在,如果存在就调用这个get方法否则直接返回val值
然后再 判断 Dep.target 是否存在,我们看看源码里面的注释对这个属性的解释
// the current target watcher being evaluated.// this is globally unique because there could be only one// watcher being evaluated at any time.Dep.target = null
这个属性表示当前正在计算的watcher, 并且在用一个时间内整个全局内只会同时存在一个
就是说:正在运行 get方法的watcher,而get是一个方法,而js是单线程,不会同一时间运行多个方法,那当运行这个get方法过程中又可能存在调用其他watcher的get方法的情况,
比如一个vm组件的render方法里面包含computed的属性的时候,这又是如何处理的呢,请看代码
const targetStack = []export function pushTarget (_target: Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target}export function popTarget () { Dep.target = targetStack.pop()}
首先定义了一个target栈,是通过一个数组实现的,
运行watcher的get方法的时候 会先调用pushTarget方法,方法里面会 判断Dep.target,如果存在则把它压进栈里面,然后把新的taget赋值给Dep.target
watcher的个体方法后面会运行popTarget,把target栈的最后一个元素赋值给Dep.target,当然如果不存在,那么Dep.target为空
通过代码可以看出来,Dep永远是对应当前正在计算的watcher,如果还不懂的话也没关系,最后会通过一个例子把整个相关的流程都解释一遍,
我们回到set方法实现:
判断Dep.target是否存在,如果存在那么表示,我获取这个属性的值的是在运行watcher的get方法过程中(因为只有运行watcher的get方法,才会pushTarget,
而且在get方法后面还会运行popTarget把Dep.target重置),那么代表这个属性值是和这个watcher是有关联的,
好,既然是有关联的,那么dep 就和watcher要进行关联操作了,即执行dep.depend方法,这个方法实现了,把dep放进watcher的newDeps里面,为什么是newDeps
而不是deps呢,因为deps代表当前的依赖,而newDeps是表示新的依赖,即运行完这次更新之后,这个watcher的相关依赖,而运行get方法的时候会清楚当前的deps里面的依赖,
把newDeps里面的依赖复制到deps里面去
如果有childOb的话,也会把childOb同watcher关联,因为如果有childOb,那么这个value肯定是一个对象,操作这个对象有两个方式,要么获取value对象的子属性,要么设置
这个对象为新对象,这和childOb都有关系,所以需要把childOb同watcher关联
再判断value值是否为array,如果是的话,就去关联array下面的对象
以上就是整个dep 和watcher 关联的部分了
而set实现就是实现通知关联的watcher进行更新,因为只有数据改变的时候,才会需要更新,所以把通知更新实现放在set方法里面
set具体实现:
当改变一个属性对应的值的时候, 先获取现在的值并做比较,如果相同就退出
然后不同的话,那么就进行复制操作
然后再执行更新操作,而在watcher部分说过,watcher的getter有3个类型:1:vm的render方法,2:computed的属性,3:watch的属性
这个时候,大多数情况是执行vm。render进行更新,当然这个render只是返回一个vNode(虚拟节点)然后调用vm的update进行真正的更新,
这里为了简化理解,所以这样说
上面就是整个关联的部分,在总结一下过程就是:在创建watcher进行的时候,1和3方式的会立即去运行get方法,然后和相关运算用到的属性对应的dep建立关联,
而2对应的computed的属性因为是懒更新的,所以在创建watcher的时候并不会立即执行关联,而是在调用的时候才会用到,而一般是在vm.render方法里面会有调用
双向绑定到这里就解释完了,本来还想继续在这里用一个例子来说明整个过程,但是发现貌似这篇文字已经很多了,那就放在下一篇《双向绑定续》里面来解释
注:因为是第一次写博客,所以可能一些东西没考虑完善,或者错误的地方,请指正,谢谢