精华内容
下载资源
问答
  • 继上一篇:深入理解Vue的watch实现原理及其实现方式 继续讲解Vue的computed实现相对于watch和data来说比较难以理解,要真正的理解computed的工作方式,你需要深入理解Vue的双向数据绑定原理和实现方式。如果你还不是...

    继上一篇:深入理解Vue的watch实现原理及其实现方式 继续讲解

    Vue的computed实现相对于watch和data来说比较难以理解,要真正的理解computed的工作方式,你需要深入理解Vue的双向数据绑定原理和实现方式。

    如果你还不是很理解推荐你先看此文章:

    彻底搞懂Vue针对数组和双向绑定(MVVM)的处理方式


    首先来看一波Vue中computed的使用方式:

    var vm = new Vue({
      data: { a: 1 },
      computed: {
        // 仅读取
        aDouble: function () {
          return this.a * 2
        },
        // 读取和设置
        aPlus: {
          get: function () {
            return this.a + 1
          },
          set: function (v) {
            this.a = v - 1
          }
        }
      }
    })
    vm.aPlus   // => 2
    vm.aPlus = 3
    vm.a       // => 2
    vm.aDouble // => 4复制代码

    计算属性的主要应用场景是代替模板内的表达式,或者data值的任何复杂逻辑都应该使用computed来计算,它有两大优势:

    1、逻辑清晰,方便于管理

    2、计算值会被缓存,依赖的data值改变时才会从新计算

    此文我们需要核心理解的是:

    1、computed是如何初始化,初始化之后干了些什么

    2、为何触发data值改变时computed会从新计算

    3、computed值为什么说是被缓存的呢,如何做的


    如果以上三个问题你都已知,你可以忽略下文了,若未知或一知半解,请抱着学习的态度看看别人的理解。

    备注:以下只是我的个人理解,并不保证绝对的正确性,若有问题欢迎指正

    以下大部分代码摘自Vue源码。


    如果你看到了这里,就当做你已经深入理解了Vue的MVVM原理及其实现方式。相关Vue的MVVM实现直接取自上一篇文章。

    Dep代码的实现:

    //标识当前的Dep id
    let uidep = 0
    class Dep{
    	constructor () {
    		this.id = uidep++
    		// 存放所有的监听watcher
        	this.subs = []
      	}
    
      	//添加一个观察者对象
      	addSub (Watcher) {
        	this.subs.push(Watcher)
      	}
      	//依赖收集
    	depend () {
    		//Dep.target 作用只有需要的才会收集依赖
    	    if (Dep.target) {
    	      Dep.target.addDep(this)
    	    }
    	}
    
    	// 调用依赖收集的Watcher更新
        notify () {
    	    const subs = this.subs.slice()
    	    for (let i = 0, l = subs.length; i < l; i++) {
    	      subs[i].update()
    	    }
      	}
    }
    
    Dep.target = null
    const targetStack = []
    
    // 为Dep.target 赋值
    function pushTarget (Watcher) {
    	if (Dep.target) targetStack.push(Dep.target)
      	Dep.target = Watcher
    }
    function popTarget () {
      Dep.target = targetStack.pop()
    }复制代码


    Watcher代码的实现:

    //去重 防止重复收集
    let uid = 0
    class Watcher{
    	constructor(vm,expOrFn,cb,options){
    		//传进来的对象 例如Vue
    		this.vm = vm
    		if (options) {
    	      this.deep = !!options.deep
    	      this.user = !!options.user
    	      this.lazy = !!options.lazy
    	    }else{
    	    	this.deep = this.user = this.lazy = false
    	    }
    	    this.dirty = this.lazy
    		//在Vue中cb是更新视图的核心,调用diff并更新视图的过程
    		this.cb = cb
    		this.id = ++uid
    		this.deps = []
    	    this.newDeps = []
    	    this.depIds = new Set()
    	    this.newDepIds = new Set()
    		if (typeof expOrFn === 'function') {
    			//data依赖收集走此处
    	      	this.getter = expOrFn
    	    } else {
    	    	//watch依赖走此处
    	      	this.getter = this.parsePath(expOrFn)
    	    }
    		//设置Dep.target的值,依赖收集时的watcher对象
    		this.value = this.lazy ? undefined : this.get()
    	}
    
    	get(){
    		//设置Dep.target值,用以依赖收集
    	    pushTarget(this)
    	    const vm = this.vm
    	    //此处会进行依赖收集 会调用data数据的 get
    	    let value = this.getter.call(vm, vm)
    	    popTarget()
    	    return value
    	}
    
    	//添加依赖
      	addDep (dep) {
      		//去重
      		const id = dep.id
    	    if (!this.newDepIds.has(id)) {
    	      	this.newDepIds.add(id)
    	      	this.newDeps.push(dep)
    	      	if (!this.depIds.has(id)) {
    	      		//收集watcher 每次data数据 set
    	      		//时会遍历收集的watcher依赖进行相应视图更新或执行watch监听函数等操作
    	        	dep.addSub(this)
    	      	}
    	    }
      	}
    
      	//更新
      	update () {
      		if (this.lazy) {
          		this.dirty = true
        	}else{
        		this.run()
        	}
    	}
    
    	//更新视图
    	run(){
    		console.log(`这里会去执行Vue的diff相关方法,进而更新数据`)
    		const value = this.get()
    		const oldValue = this.value
            this.value = value
    		if (this.user) {
    			//watch 监听走此处
                this.cb.call(this.vm, value, oldValue)
            }else{
            	//data 监听走此处
            	//这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图
    			this.cb.call(this.vm, value, oldValue)
            }
    	}
    
        //如果计算熟悉依赖的data值发生变化时会调用
        //案例中 当data.name值发生变化时会执行此方法
    	evaluate () {
    	    this.value = this.get()
    	    this.dirty = false
    	}
    	//收集依赖
    	depend () {
    	    let i = this.deps.length
    	    while (i--) {
    	      this.deps[i].depend()
    	    }
    	}
    
    	// 此方法获得每个watch中key在data中对应的value值
    	//使用split('.')是为了得到 像'a.b.c' 这样的监听值
    	parsePath (path){
    		const bailRE = /[^w.$]/
    	  if (bailRE.test(path)) return
    	  	const segments = path.split('.')
    	  	return function (obj) {
    		    for (let i = 0; i < segments.length; i++) {
    		      	if (!obj) return
    		      	//此处为了兼容我的代码做了一点修改	 
    		        //此处使用新获得的值覆盖传入的值 因此能够处理 'a.b.c'这样的监听方式
    		        if(i==0){
    		        	obj = obj.data[segments[i]]
    		        }else{
    		        	obj = obj[segments[i]]
    		        }
    		    }
    		    return obj
    		 }
    	}
    }复制代码

    在Watcher中对于computed来说核心注意点是以下方法:

    //如果计算熟悉依赖的data值发生变化时会调用
    //案例中 当data.name值发生变化时会执行此方法
    evaluate () {
        this.value = this.get()
        this.dirty = false
    }复制代码

    当computed中用到的data值发生变化时,视图更新调用computed值时会从新执行,获得新的计算属性值。


    Observer代码实现

    class Observer{
    	constructor (value) {
    	    this.value = value
    	    // 增加dep属性(处理数组时可以直接调用)
    	    this.dep = new Dep()
    	    //将Observer实例绑定到data的__ob__属性上面去,后期如果oberve时直接使用,不需要从新Observer,
    	    //处理数组是也可直接获取Observer对象
    	    def(value, '__ob__', this)
    	    if (Array.isArray(value)) {
    	    	//这里只测试对象
    	    } else {
    	    	//处理对象
    	      	this.walk(value)
    	    }
    	}
    
    	walk (obj) {
        	const keys = Object.keys(obj)
        	for (let i = 0; i < keys.length; i++) {
        		//此处我做了拦截处理,防止死循环,Vue中在oberve函数中进行的处理
        		if(keys[i]=='__ob__') return;
          		defineReactive(obj, keys[i], obj[keys[i]])
        	}
      	}
    }
    //数据重复Observer
    function observe(value){
    	if(typeof(value) != 'object' ) return;
    	let ob = new Observer(value)
      	return ob;
    }
    // 把对象属性改为getter/setter,并收集依赖
    function defineReactive (obj,key,val) {
      	const dep = new Dep()
      	//处理children
      	let childOb = observe(val)
      	Object.defineProperty(obj, key, {
        	enumerable: true,
        	configurable: true,
        	get: function reactiveGetter () {
        		console.log(`调用get获取值,值为${val}`)
          		const value = val
          		if (Dep.target) {
    	        	dep.depend()
    		        if (childOb) {
    		          	childOb.dep.depend()
    		        }
    	      	}
          		return value
    	    },
    	    set: function reactiveSetter (newVal) {
    	    	console.log(`调用了set,值为${newVal}`)
    	      	const value = val
    	       	val = newVal
    	       	//对新值进行observe
    	      	childOb = observe(newVal)
    	      	//通知dep调用,循环调用手机的Watcher依赖,进行视图的更新
    	      	dep.notify()
    	    }
      })
    }
    //辅助方法
    function def (obj, key, val) {
      Object.defineProperty(obj, key, {
        value: val,
        enumerable: true,
        writable: true,
        configurable: true
      })
    }复制代码


    此文的重点Computed代码实现:

    //空函数
    const noop = ()=>{}
    // computed初始化的Watcher传入lazy: true就会触发Watcher中的dirty值为true
    const computedWatcherOptions = { lazy: true }
    //Object.defineProperty 默认value参数
    const sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: noop,
      set: noop
    }
    // 初始化computed
    class initComputed {
    	constructor(vm, computed){
    		//新建存储watcher对象,挂载在vm对象执行
    		const watchers = vm._computedWatchers = Object.create(null)
    		//遍历computed
    		for (const key in computed) {
    		    const userDef = computed[key]
    		    //getter值为computed中key的监听函数或对象的get值
    		    let getter = typeof userDef === 'function' ? userDef : userDef.get
    		    //新建computed的 watcher
    		    watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
    		    if (!(key in vm)) {
    		      	/*定义计算属性*/
    		      	this.defineComputed(vm, key, userDef)
    		    }
    		}
    	}
        //把计算属性的key挂载到vm对象下,并使用Object.defineProperty进行处理
        //因此调用vm.somecomputed 就会触发get函数
    	defineComputed (target, key, userDef) {
    	  if (typeof userDef === 'function') {
    	    sharedPropertyDefinition.get = this.createComputedGetter(key)
    	    sharedPropertyDefinition.set = noop
    	  } else {
    	    sharedPropertyDefinition.get = userDef.get
    	      ? userDef.cache !== false
    	        ? this.createComputedGetter(key)
    	        : userDef.get
    	      : noop
    	      //如果有设置set方法则直接使用,否则赋值空函数
    	    	sharedPropertyDefinition.set = userDef.set
    	      	? userDef.set
    	      	: noop
    	  }
    	  Object.defineProperty(target, key, sharedPropertyDefinition)
    	}
    
    	//计算属性的getter 获取计算属性的值时会调用
    	createComputedGetter (key) {
    	  return function computedGetter () {
    	  	//获取到相应的watcher
    	    const watcher = this._computedWatchers && this._computedWatchers[key]
    	    if (watcher) {
    	    	//watcher.dirty 参数决定了计算属性值是否需要重新计算,默认值为true,即第一次时会调用一次
    	      	if (watcher.dirty) {
    	      		/*每次执行之后watcher.dirty会设置为false,只要依赖的data值改变时才会触发
    	      		watcher.dirty为true,从而获取值时从新计算*/
    	        	watcher.evaluate()
    	      	}
    	      	//获取依赖
    	      	if (Dep.target) {
    	        	watcher.depend()
    	      	}
    	      	//返回计算属性的值
    	      	return watcher.value
    	    }
    	  }
    	}
    }复制代码


    代码已经写完,完整代码如下:

    //标识当前的Dep id
    let uidep = 0
    class Dep{
    	constructor () {
    		this.id = uidep++
    		// 存放所有的监听watcher
        	this.subs = []
      	}
    
      	//添加一个观察者对象
      	addSub (Watcher) {
        	this.subs.push(Watcher)
      	}
      	//依赖收集
    	depend () {
    		//Dep.target 作用只有需要的才会收集依赖
    	    if (Dep.target) {
    	      Dep.target.addDep(this)
    	    }
    	}
    
    	// 调用依赖收集的Watcher更新
        notify () {
    	    const subs = this.subs.slice()
    	    for (let i = 0, l = subs.length; i < l; i++) {
    	      subs[i].update()
    	    }
      	}
    }
    
    Dep.target = null
    const targetStack = []
    
    // 为Dep.target 赋值
    function pushTarget (Watcher) {
    	if (Dep.target) targetStack.push(Dep.target)
      	Dep.target = Watcher
    }
    function popTarget () {
      Dep.target = targetStack.pop()
    }
    /*----------------------------------------Watcher------------------------------------*/
    //去重 防止重复收集
    let uid = 0
    class Watcher{
    	constructor(vm,expOrFn,cb,options){
    		//传进来的对象 例如Vue
    		this.vm = vm
    		if (options) {
    	      this.deep = !!options.deep
    	      this.user = !!options.user
    	      this.lazy = !!options.lazy
    	    }else{
    	    	this.deep = this.user = this.lazy = false
    	    }
    	    this.dirty = this.lazy
    		//在Vue中cb是更新视图的核心,调用diff并更新视图的过程
    		this.cb = cb
    		this.id = ++uid
    		this.deps = []
    	    this.newDeps = []
    	    this.depIds = new Set()
    	    this.newDepIds = new Set()
    		if (typeof expOrFn === 'function') {
    			//data依赖收集走此处
    	      	this.getter = expOrFn
    	    } else {
    	    	//watch依赖走此处
    	      	this.getter = this.parsePath(expOrFn)
    	    }
    		//设置Dep.target的值,依赖收集时的watcher对象
    		this.value = this.lazy ? undefined : this.get()
    	}
    
    	get(){
    		//设置Dep.target值,用以依赖收集
    	    pushTarget(this)
    	    const vm = this.vm
    	    //此处会进行依赖收集 会调用data数据的 get
    	    let value = this.getter.call(vm, vm)
    	    popTarget()
    	    return value
    	}
    
    	//添加依赖
      	addDep (dep) {
      		//去重
      		const id = dep.id
    	    if (!this.newDepIds.has(id)) {
    	      	this.newDepIds.add(id)
    	      	this.newDeps.push(dep)
    	      	if (!this.depIds.has(id)) {
    	      		//收集watcher 每次data数据 set
    	      		//时会遍历收集的watcher依赖进行相应视图更新或执行watch监听函数等操作
    	        	dep.addSub(this)
    	      	}
    	    }
      	}
    
      	//更新
      	update () {
      		if (this.lazy) {
          		this.dirty = true
        	}else{
        		this.run()
        	}
    	}
    
    	//更新视图
    	run(){
    		console.log(`这里会去执行Vue的diff相关方法,进而更新数据`)
    		const value = this.get()
    		const oldValue = this.value
            this.value = value
    		if (this.user) {
    			//watch 监听走此处
                this.cb.call(this.vm, value, oldValue)
            }else{
            	//data 监听走此处
            	//这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图
    			this.cb.call(this.vm, value, oldValue)
            }
    	}
    
        //如果计算熟悉依赖的data值发生变化时会调用
        //案例中 当data.name值发生变化时会执行此方法
    	evaluate () {
    	    this.value = this.get()
    	    this.dirty = false
    	}
    	//收集依赖
    	depend () {
    	    let i = this.deps.length
    	    while (i--) {
    	      this.deps[i].depend()
    	    }
    	}
    
    	// 此方法获得每个watch中key在data中对应的value值
    	//使用split('.')是为了得到 像'a.b.c' 这样的监听值
    	parsePath (path){
    		const bailRE = /[^w.$]/
    	  if (bailRE.test(path)) return
    	  	const segments = path.split('.')
    	  	return function (obj) {
    		    for (let i = 0; i < segments.length; i++) {
    		      	if (!obj) return
    		      	//此处为了兼容我的代码做了一点修改	 
    		        //此处使用新获得的值覆盖传入的值 因此能够处理 'a.b.c'这样的监听方式
    		        if(i==0){
    		        	obj = obj.data[segments[i]]
    		        }else{
    		        	obj = obj[segments[i]]
    		        }
    		    }
    		    return obj
    		 }
    	}
    }
    
    /*----------------------------------------Observer------------------------------------*/
    class Observer{
    	constructor (value) {
    	    this.value = value
    	    // 增加dep属性(处理数组时可以直接调用)
    	    this.dep = new Dep()
    	    //将Observer实例绑定到data的__ob__属性上面去,后期如果oberve时直接使用,不需要从新Observer,
    	    //处理数组是也可直接获取Observer对象
    	    def(value, '__ob__', this)
    	    if (Array.isArray(value)) {
    	    	//这里只测试对象
    	    } else {
    	    	//处理对象
    	      	this.walk(value)
    	    }
    	}
    
    	walk (obj) {
        	const keys = Object.keys(obj)
        	for (let i = 0; i < keys.length; i++) {
        		//此处我做了拦截处理,防止死循环,Vue中在oberve函数中进行的处理
        		if(keys[i]=='__ob__') return;
          		defineReactive(obj, keys[i], obj[keys[i]])
        	}
      	}
    }
    //数据重复Observer
    function observe(value){
    	if(typeof(value) != 'object' ) return;
    	let ob = new Observer(value)
      	return ob;
    }
    // 把对象属性改为getter/setter,并收集依赖
    function defineReactive (obj,key,val) {
      	const dep = new Dep()
      	//处理children
      	let childOb = observe(val)
      	Object.defineProperty(obj, key, {
        	enumerable: true,
        	configurable: true,
        	get: function reactiveGetter () {
        		console.log(`调用get获取值,值为${val}`)
          		const value = val
          		if (Dep.target) {
    	        	dep.depend()
    		        if (childOb) {
    		          	childOb.dep.depend()
    		        }
    	      	}
          		return value
    	    },
    	    set: function reactiveSetter (newVal) {
    	    	console.log(`调用了set,值为${newVal}`)
    	      	const value = val
    	       	val = newVal
    	       	//对新值进行observe
    	      	childOb = observe(newVal)
    	      	//通知dep调用,循环调用手机的Watcher依赖,进行视图的更新
    	      	dep.notify()
    	    }
      })
    }
    //辅助方法
    function def (obj, key, val) {
      Object.defineProperty(obj, key, {
        value: val,
        enumerable: true,
        writable: true,
        configurable: true
      })
    }
    /*----------------------------------------初始化watch------------------------------------*/
    //空函数
    const noop = ()=>{}
    // computed初始化的Watcher传入lazy: true就会触发Watcher中的dirty值为true
    const computedWatcherOptions = { lazy: true }
    //Object.defineProperty 默认value参数
    const sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: noop,
      set: noop
    }
    // 初始化computed
    class initComputed {
    	constructor(vm, computed){
    		//新建存储watcher对象,挂载在vm对象执行
    		const watchers = vm._computedWatchers = Object.create(null)
    		//遍历computed
    		for (const key in computed) {
    		    const userDef = computed[key]
    		    //getter值为computed中key的监听函数或对象的get值
    		    let getter = typeof userDef === 'function' ? userDef : userDef.get
    		    //新建computed的 watcher
    		    watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
    		    if (!(key in vm)) {
    		      	/*定义计算属性*/
    		      	this.defineComputed(vm, key, userDef)
    		    }
    		}
    	}
        //把计算属性的key挂载到vm对象下,并使用Object.defineProperty进行处理
        //因此调用vm.somecomputed 就会触发get函数
    	defineComputed (target, key, userDef) {
    	  if (typeof userDef === 'function') {
    	    sharedPropertyDefinition.get = this.createComputedGetter(key)
    	    sharedPropertyDefinition.set = noop
    	  } else {
    	    sharedPropertyDefinition.get = userDef.get
    	      ? userDef.cache !== false
    	        ? this.createComputedGetter(key)
    	        : userDef.get
    	      : noop
    	      //如果有设置set方法则直接使用,否则赋值空函数
    	    	sharedPropertyDefinition.set = userDef.set
    	      	? userDef.set
    	      	: noop
    	  }
    	  Object.defineProperty(target, key, sharedPropertyDefinition)
    	}
    
    	//计算属性的getter 获取计算属性的值时会调用
    	createComputedGetter (key) {
    	  return function computedGetter () {
    	  	//获取到相应的watcher
    	    const watcher = this._computedWatchers && this._computedWatchers[key]
    	    if (watcher) {
    	    	//watcher.dirty 参数决定了计算属性值是否需要重新计算,默认值为true,即第一次时会调用一次
    	      	if (watcher.dirty) {
    	      		/*每次执行之后watcher.dirty会设置为false,只要依赖的data值改变时才会触发
    	      		watcher.dirty为true,从而获取值时从新计算*/
    	        	watcher.evaluate()
    	      	}
    	      	//获取依赖
    	      	if (Dep.target) {
    	        	watcher.depend()
    	      	}
    	      	//返回计算属性的值
    	      	return watcher.value
    	    }
    	  }
    	}
    }复制代码


    computed测试:

    //1、首先来创建一个Vue构造函数:
    function Vue(){
    }
    //2、设置data和computed的值:
    let data={
        name:'Hello',
    }
    let computed={
    	getfullname:function(){
    		console.log('-----走了computed 之 getfullname------')
    		console.log('新的值为:'+data.name + ' - world')
    		return data.name + ' - world'
    	}
    }
    //3、实例化Vue并把data挂载到Vue上
    let vue 		= new Vue()
    vue.data 		= data
    //4、创建Watcher对象
    let updateComponent = (vm)=>{
    	// 收集依赖
    	data.name
    	
    }
    let watcher1 = new Watcher(vue,updateComponent,()=>{})
    //5、初始化Data并收集依赖
    observe(data)
    //6、初始化computed
    let watcher2 = new initComputed(vue,computed)复制代码


    在浏览器console中测试:

    //首先获得一次getfullname
    vue.getfullname
    
    //第二次调用getfullname看看会有什么变化呢
    vue.getfullname复制代码

    分析:调用vue.getfullname第一次会打印 '-----走了computed 之 getfullname------',即计算属性第一次计算了值,第二次调用时,不会再打印值

    即直接获取的缓存值,为什么第二次是获得的缓存值呢,因为第二次执行时watcher.dirty=true,就会直接返回watcher.value值。


    //为data.name赋值
    data.name = 'Hi'复制代码

    分析:执行data.name时会触发两个Watcher监听函数(为什么是两个Watcher自己去脑补一下额!),一个是全局的watcher,一个是computed的watcher,第一个Watcher会更新视图,第二个Watcher会触发watcher.dirty=true。


    //name值变更之后再次执行会是什么结果呢
    vue.getfullname
    
    //再执行一次
    vue.getfullname复制代码

    分析:运行vue.getfullname时会执行computedGetter函数,因为watcher.dirty=true因此会从新计算值,因此会打印 '-----走了computed 之 getfullname------',值为'HI world', 再次执行只会获得计算属性的缓存值。

    所有测试代码如下:

    /*----------------------------------------Vue------------------------------------*/
    function Vue(){
    }
    /*----------------------------------------测试代码------------------------------------*/
    // 调用
    let data={
        name:'Hello',
    }
    let computed={
    	getfullname:function(){
    		console.log('-----走了computed 之 getfullname------')
    		console.log('新的值为:'+data.name + ' - world')
    		return data.name + ' - world'
    	}
    }
    let vue 		= new Vue()
    vue.data 		= data
    let updateComponent = (vm)=>{
    	// 收集依赖
    	data.name
    }
    let watcher1 = new Watcher(vue,updateComponent,()=>{})
    observe(data)
    let watvher2 = new initComputed(vue,computed)
    
    //测试 浏览器console中相继运行一下代码测试
    vue.getfullname
    vue.getfullname
    data.name='Hi'
    vue.getfullname
    vue.getfullname复制代码


    若有疑问欢迎交流。


    展开全文
  • 浅谈 Vuecomputed 实现原理

    千次阅读 2018-10-10 19:05:26
    虽然目前技术栈已由 Vue 转到了 React,但从之前使用 Vue 开发多个项目实际经历来看还是非常愉悦Vue 文档清晰规范,api 设计简洁高效,对前端开发人员友好,上手快,甚至个人认为在很多场景使用 Vue 比React...

    虽然目前的技术栈已由 Vue 转到了 React,但从之前使用 Vue 开发的多个项目实际经历来看还是非常愉悦的,Vue 文档清晰规范,api 设计简洁高效,对前端开发人员友好,上手快,甚至个人认为在很多场景使用 Vue 比React 开发效率更高,之前也有断断续续研读过 Vue 的源码,但一直没有梳理总结,所以在此做一些技术归纳同时也加深自己对 Vue 的理解,那么今天要写的便是 Vue 中最常用到的 API 之一 computed 的实现原理。

    基本介绍

    话不多说,一个最基本的例子如下:

    {{fullName}}
    
    new Vue({
        data: {
            firstName: 'Xiao',
            lastName: 'Ming'
        },
        computed: {
            fullName: function () {
                return this.firstName + ' ' + this.lastName
            }
        }
    })
    

    Vue 中我们不需要在 template 里面直接计算 {{this.firstName + ‘ ‘ + this.lastName}},因为在模版中放入太多声明式的逻辑会让模板本身过重,尤其当在页面中使用大量复杂的逻辑表达式处理数据时,会对页面的可维护性造成很大的影响,而 computed 的设计初衷也正是用于解决此类问题。

    对比侦听器 watch

    当然很多时候我们使用 computed 时往往会与 Vue 中另一个 API 也就是侦听器 watch 相比较,因为在某些方面它们是一致的,都是以 Vue 的依赖追踪机制为基础,当某个依赖数据发生变化时,所有依赖这个数据的相关数据或函数都会自动发生变化或调用。

    虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch
    选项提供了一个更通用的方法来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

    从 Vue 官方文档对 watch 的解释我们可以了解到,使用 watch 选项允许我们执行异步操作(访问一个 API)或高消耗性能的操作,限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态,而这些都是计算属性无法做到的。

    下面还另外总结了几点关于 computed 和 watch 的差异:

    • computed 是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 watch 是监听已经存在且已挂载到 vm
      上的数据,所以用 watch 同样可以监听 computed 计算属性的变化(其它还有 data、props)
    • computed 本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值,而
      watch 则是当数据发生变化便会调用执行函数
    • 从使用场景上说,computed 适用一个数据被多个数据影响,而 watch 适用一个数据影响多个数据;

    以上我们了解了 computed 和 watch 之间的一些差异和使用场景的区别,当然某些时候两者并没有那么明确严格的限制,最后还是要具体到不同的业务进行分析。

    原理分析

    言归正传,回到文章的主题 computed 身上,为了更深层次地了解计算属性的内在机制,接下来就让我们一步步探索 Vue 源码中关于它的实现原理吧。

    在分析 computed 源码之前我们先得对 Vue 的响应式系统有一个基本的了解,Vue 称其为非侵入性的响应式系统,数据模型仅仅是普通的 JavaScript 对象,而当你修改它们时,视图便会进行自动更新。
    在这里插入图片描述

    当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项时,Vue 将遍历此对象所有的属性,并使用
    Object.defineProperty 把这些属性全部转为 getter/setter,这些 getter/setter
    对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化,每个组件实例都有相应的 watcher
    实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher
    重新计算,从而致使它关联的组件得以更新。

    Vue 响应系统,其核心有三点:observe、watcher、dep:

    • observe:遍历 data 中的属性,使用 Object.defineProperty 的 get/set 方法对其进行数据劫持;
    • dep:每个属性拥有自己的消息订阅器 dep,用于存放所有订阅了该属性的观察者对象;
    • watcher:观察者(对象),通过 dep 实现对响应属性的监听,监听到结果后,主动触发自己的回调进行响应。

    对响应式系统有一个初步了解后,我们再来分析计算属性。 首先我们找到计算属性的初始化是在 src/core/instance/state.js 文件中的 initState 函数中完成的

    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 */)
      }
      // computed初始化
      if (opts.computed) initComputed(vm, opts.computed)
      if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
      }
    }
    

    调用了 initComputed 函数(其前后也分别初始化了 initData 和 initWatch )并传入两个参数 vm 实例和 opt.computed 开发者定义的 computed 选项,转到 initComputed 函数:

    const computedWatcherOptions = { computed: true }
    function initComputed (vm: Component, computed: Object) {
      // $flow-disable-line
      const watchers = vm._computedWatchers = Object.create(null)
      // computed properties are just getters during SSR
      const isSSR = isServerRendering()
    
      for (const key in computed) {
        const userDef = computed[key]
        const getter = typeof userDef === 'function' ? userDef : userDef.get
        if (process.env.NODE_ENV !== 'production' && getter == null) {
          warn(
            'Getter is missing for computed property "${key}".',
            vm      )
        }
    
        if (!isSSR) {
          // create internal watcher for the computed property.
          watchers[key] = new Watcher(
            vm,
            getter || noop,
            noop,
            computedWatcherOptions      )
        }
    
        // component-defined computed properties are already defined on the
        // component prototype. We only need to define computed properties defined
        // at instantiation here.
        if (!(key in vm)) {
          defineComputed(vm, key, userDef)
        } else if (process.env.NODE_ENV !== 'production') {
          if (key in vm.$data) {
            warn('The computed property "${key}" is already defined in data.', vm)
          } else if (vm.$options.props && key in vm.$options.props) {
            warn('The computed property "${key}" is already defined as a prop.', vm)
          }
        }
      }
    }
    

    从这段代码开始我们观察这几部分:

    获取计算属性的定义 userDef 和 getter 求值函数

       const userDef = computed[key]
        const getter = typeof userDef === 'function' ? userDef : userDef.get
    

    定义一个计算属性有两种写法,一种是直接跟一个函数,另一种是添加 set 和 get 方法的对象形式,所以这里首先获取计算属性的定义 userDef,再根据 userDef 的类型获取相应的 getter 求值函数。

    计算属性的观察者 watcher 和消息订阅器 dep

    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions)
    

    这里的 watchers 也就是 vm._computedWatchers 对象的引用,存放了每个计算属性的观察者 watcher 实例(注:后文中提到的“计算属性的观察者”、“订阅者”和 watcher 均指代同一个意思但注意和 Watcher 构造函数区分),Watcher 构造函数在实例化时传入了 4 个参数:vm 实例、getter求值函数、noop 空函数、computedWatcherOptions 常量对象(在这里提供给 Watcher 一个标识 {computed:true} 项,表明这是一个计算属性而不是非计算属性的观察者,我们来到 Watcher 构造函数的定义:

    class Watcher {
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        if (options) {
          this.computed = !!options.computed
        } 
    
        if (this.computed) {
          this.value = undefined
          this.dep = new Dep()
        } else {
          this.value = this.get()
        }
      }
    
      get () {
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          value = this.getter.call(vm, vm)
        } catch (e) {
    
        } finally {
          popTarget()
        }
        return value
      }
    
      update () {
        if (this.computed) {
          if (this.dep.subs.length === 0) {
            this.dirty = true
          } else {
            this.getAndInvoke(() => {
              this.dep.notify()
            })
          }
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    
      evaluate () {
        if (this.dirty) {
          this.value = this.get()
          this.dirty = false
        }
        return this.value
      }
    
      depend () {
        if (this.dep && Dep.target) {
          this.dep.depend()
        }
      }
    }
    

    为了简洁突出重点,这里我手动去掉了我们暂时不需要关心的代码片段。 观察 Watcher 的 constructor ,结合刚才讲到的 new Watcher 传入的第四个参数 {computed:true} 知道,对于计算属性而言 watcher 会执行 if 条件成立的代码 this.dep = new Dep(),而 dep 也就是创建了该属性的消息订阅器。

     export default class Dep {
          static target: ?Watcher;
          subs: Array<Watcher>;
        
          constructor () {
            this.id = uid++
            this.subs = []
          }
        
          addSub (sub: Watcher) {
            this.subs.push(sub)
          }
        
          depend () {
            if (Dep.target) {
              Dep.target.addDep(this)
            }
          }
        
          notify () {
            const subs = this.subs.slice()
            for (let i = 0, l = subs.length; i < l; i++) {
              subs[i].update()
            }
          }
        }
        Dep.target = null
    

    Dep 同样精简了部分代码,我们观察 Watcher 和 Dep 的关系,用一句话总结

    watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个
    watcher 更新。

    defineComputed 定义计算属性

    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn('The computed property "${key}" is already defined in data.', vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn('The computed property "${key}" is already defined as a prop.', vm)
      }
    }
    

    因为 computed 属性是直接挂载到实例对象中的,所以在定义之前需要判断对象中是否已经存在重名的属性,defineComputed 传入了三个参数:vm实例、计算属性的 key 以及 userDef 计算属性的定义(对象或函数)。 然后继续找到 defineComputed 定义处:

    export function defineComputed (
      target: any,
      key: string,
      userDef: Object | Function
    ) {
      const shouldCache = !isServerRendering()
      if (typeof userDef === 'function') {
        sharedPropertyDefinition.get = shouldCache
          ? createComputedGetter(key)
          : userDef
        sharedPropertyDefinition.set = noop
      } else {
        sharedPropertyDefinition.get = userDef.get
          ? shouldCache && userDef.cache !== false
            ? createComputedGetter(key)
            : userDef.get
          : noop
        sharedPropertyDefinition.set = userDef.set
          ? userDef.set
          : noop
      }
      if (process.env.NODE_ENV !== 'production' &&
          sharedPropertyDefinition.set === noop) {
        sharedPropertyDefinition.set = function () {
          warn(
            'Computed property "${key}" was assigned to but it has no setter.',
            this
          )
        }
      }
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    

    在这段代码的最后调用了原生 Object.defineProperty 方法,其中传入的第三个参数是属性描述符sharedPropertyDefinition,初始化为:

    const sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: noop,
      set: noop
    }
    

    随后根据 Object.defineProperty 前面的代码可以看到 sharedPropertyDefinition 的 get/set 方法在经过 userDef 和 shouldCache 等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinition 的 get 函数也就是 createComputedGetter(key) 的结果,我们找到 createComputedGetter 函数调用结果并最终改写 sharedPropertyDefinition 大致呈现如下:

    sharedPropertyDefinition = {
        enumerable: true,
        configurable: true,
        get: function computedGetter () {
            const watcher = this._computedWatchers && this._computedWatchers[key]
            if (watcher) {
                watcher.depend()
                return watcher.evaluate()
            }
        },
        set: userDef.set || noop
    }
    

    当计算属性被调用时便会执行 get 访问函数,从而关联上观察者对象 watcher 然后执行 wather.depend() 收集依赖和 watcher.evaluate() 计算求值。

    分析完所有步骤,我们再来总结下整个流程:

    • 当组件初始化的时候,computed 和 data 会分别建立各自的响应系统,Observer遍历 data 中每个属性设置
      get/set 数据拦截
    • 初始化 computed 会调用 initComputed 函数
      • 注册一个 watcher 实例,并在内实例化一个 Dep 消息订阅器用作后续收集依赖(比如渲染函数的 watcher
        或者其他观察该计算属性变化的 watcher )
      • 调用计算属性时会触发其Object.defineProperty的get访问器函数
      • 调用 watcher.depend() 方法向自身的消息订阅器 dep 的 subs 中添加其他属性的 watcher
      • 调用 watcher 的 evaluate 方法(进而调用 watcher 的 get 方法)让自身成为其他 watcher
        的消息订阅器的订阅者,首先将 watcher 赋给 Dep.target,然后执行 getter
        求值函数,当访问求值函数里面的属性(比如来自 data、props 或其他 computed)时,会同样触发它们的 get访问器函数从而将该计算属性的 watcher 添加到求值函数中属性的 watcher 的消息订阅器 dep 中,当这些操作完成,最后关闭Dep.target 赋为 null 并返回求值函数结果。
    • 当某个属性发生变化,触发 set 拦截函数,然后调用自身消息订阅器 dep 的 notify 方法,遍历当前 dep 中保存着所有订阅者wathcer 的 subs 数组,并逐个调用 watcher 的 update 方法,完成响应更新。

    原文链接:https://mp.weixin.qq.com/s/igkif-J_BHd1q5mZ7TewCw

    展开全文
  • 1.初始化 data 和 computed,分别代理其 set 和 get 方法,对 data 中所有属性生成唯一 dep 实例 2.对 computed 属性生成唯一 watcher,并保存在 vm._computedWatchers 中 3.访问计算属性时,设置 Dep....

    1.初始化 data 和 computed,分别代理其 set 和 get 方法,对 data 中的所有属性生成唯一的 dep 实例

    2.对 computed 中的 属性生成唯一的 watcher,并保存在 vm._computedWatchers 中

    3.访问计算属性时,设置 Dep.target 指向 计算属性的 watcher,调用该属性具体方法

    4.方法中访问 data 的属性,即会调用 data 属性的 get 方法,将 data 属性的 dep 加入到 计算属性的 watcher , 同时该 dep 中的 subs 添加这个 watcher

    5.设置 data 的这个属性时,调用该属性代理的 set 方法,触发 dep 的 notify 方法

    6.因为时 computed 属性,只是将 watcher 中的 dirty 设置为 true

    7.最后,访问计算属性的 get 方法时,得知该属性的 watcher.dirty 为 true,则调用 watcher.evaluate() 方法获取新的值

    综合以上:也可以解释了为什么有些时候当computed没有被访问(或者没有被模板依赖),当修改了this.data值后,通过vue-tools发现其computed中的值没有变化的原因,因为没有触发到其get方法。

    computed vs methods
    计算属性跟方法都能打到同样的方法,那么他们之间有什么不同呢?我们还是看一段代码理解一下

    // methods 每次render都会重新计算
    <template>
      <p>{{getName()}}</p>
    </template>
    methods:{
      getName:function(){
        return this.name.split(' ').reverse().join(' ')
      }
    }
    // 使用computed ,computed是基于他们的依赖进行缓存的,也就是说下面这段代码只要this.name不改变,每次访问都会立即返回结果
    <template>
      <p>{{getName}}</p>
    </template>
    computed:{
      getName:function(){
        return this.name.split(' ').reverse().join(' ')
      },
    }

    总结:methods是实时的,在重新渲染时,函数总会重新调用执行,不会缓存,(多次输出时间不同)
    而computed只有在属性值发生改变时才会触发,因此 性能会更好,但是如果你不希望缓存,你可以使用 methods 属性。
    但是在利用实时信息时,比如显示当前进入页面的时间,必须用methods方式

    methods: {
        now: function () {
          return Date.now()
        }
    }

    如果用computed计算属性的话,每次进入页面将一直沿用第一次的信息,不会再触发now。

    展开全文
  • Vue computed 实现原理

    千次阅读 2019-03-03 16:35:47
    Vue computed 实现原理 Vue 2.5.17 1、Vue 在初始化时候会 initState ,这个方法会初始化 props methods data computed watch // 4069 vm._self = vm; initLifecycle(vm); initEvents(vm); initRender(vm); ...

    Vue computed 实现原理

    Vue 2.5.17

    • 1、Vue 在初始化的时候会 initState ,这个方法会初始化 props methods data computed watch
    // 4069
    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');
    
    // 3303
    function initState (vm) {
      vm._watchers = [];
      var 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);
      }
    }
    
    • 2、在初始化 computed 的时候首先会拿到对应的 get 方法。其次在非 SSR 下会创建对应的 watcher。随后对其进行初始化
    // 3422
    var computedWatcherOptions = { lazy: true };
    
    function initComputed(vm, computed) {
      var watchers = (vm._computedWatchers = Object.create(null))
      var isSSR = isServerRendering()
      for (var key in computed) {
        var userDef = computed[key]
        var getter = typeof userDef === 'function' ? userDef : userDef.get
        if ('development' !== 'production' && getter == null) {
          warn('Getter is missing for computed property "' + key + '".', vm)
        }
        if (!isSSR) {
          watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
        }
        if (!(key in vm)) {
          defineComputed(vm, key, userDef)
        } else {
          if (key in vm.$data) {
            warn('The computed property "' + key + '" is already defined in data.', vm)
          } else if (vm.$options.props && key in vm.$options.props) {
            warn('The computed property "' + key + '" is already defined as a prop.', vm)
          }
        }
      }
    }
    
    • 3、随后经过一系列的判断对其进行 Object.defineProperty 初始化。非 SSR 下会调用 createComputedGetter 方法对原始的 getter 进行加工
    // 3286
    var sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: noop,
      set: noop
    };
    
    // 3465
    function defineComputed(target, key, userDef) {
      var shouldCache = !isServerRendering()
      if (typeof userDef === 'function') {
        sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : userDef
        sharedPropertyDefinition.set = noop
      } else {
        sharedPropertyDefinition.get = userDef.get
          ? shouldCache && userDef.cache !== false
            ? createComputedGetter(key)
            : userDef.get
          : noop
        sharedPropertyDefinition.set = userDef.set ? userDef.set : noop
      }
      if ('development' !== 'production' && sharedPropertyDefinition.set === noop) {
        sharedPropertyDefinition.set = function() {
          warn('Computed property "' + key + '" was assigned to but it has no setter.', this)
        }
      }
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    
    • 4、我们来看一下 createComputedGetter 是如何加工的,在获取计算属性的值时候其实是拿的 watcher.value。在这里我们先不管 evaluate 和 depend 是干什么的我们先看一下 watcher 如何定义
    // 3489
    function createComputedGetter (key) {
      return function computedGetter () {
        var watcher = this._computedWatchers && this._computedWatchers[key];
        if (watcher) {
          if (watcher.dirty) {
            watcher.evaluate();
          }
          if (Dep.target) {
            watcher.depend();
          }
          return watcher.value
        }
      }
    }
    
    • 5、watcher 的 value 实际上是调用的 watcher.get 方法。这个方法有调用计算属性对应的 getter 方法。但我们在定义 watcher 的时候会传入 { lazy: true} ,这时候我们在定义 watcher 的时候不会触发 get 方法,而反过来看步骤 4,当 dirty = true 的时候首先会调用 watcher.evaluate()
    // 710
    Dep.target = null;
    var targetStack = [];
    function pushTarget (_target) {
      if (Dep.target) { targetStack.push(Dep.target); }
      Dep.target = _target;
    }
    function popTarget () {
      Dep.target = targetStack.pop();
    }
    
    
    // 3082
    var Watcher = function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
      // ...
      this.dirty = this.lazy;
      // ...
      this.value = this.lazy ? undefined : this.get()
    }
    
    // 3235
    Watcher.prototype.get = function get() {
      pushTarget(this);
      var value;
      var vm = this.vm;
      try {
        value = this.getter.call(vm, vm);
      } catch (e) {
        if (this.user) {
          handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
        } else {
          throw e
        }
      } finally {
        // "touch" every property so they are all tracked as
        // dependencies for deep watching
        if (this.deep) {
          traverse(value);
        }
        popTarget();
        this.cleanupDeps();
      }
      return value
    }
    
    • 6、当调用了 evaluate 方法就会计算一次 value,并将 dirty 置为 false。我们不妨设想一下:当我们的一开始的时候 dirty = true 会计算 value 随后 dirty = false,当我们的依赖项发生变化的时候将 dirty = true,这时候我们获取 value 的时候会重新计算。那么我们依赖项发生变化的时候是如何将对应的 dirty 设置为 true 的?
    // 3246
    Watcher.prototype.evaluate = function evaluate () {
      this.value = this.get();
      this.dirty = false;
    };
    
    • 7、我们在初始化 data 或者 props 的时候都会触发 defineReactive 方法
    // 966
    function defineReactive(obj, key, val, customSetter, shallow) {
      // ...
      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 ('development' !== 'production' && customSetter) {
            customSetter()
          }
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          childOb = !shallow && observe(newVal)
          dep.notify()
        }
      })
    }
    
    
    • 8、在我们调用 watcher.get 的时候会调用 pushTarget 方法,将 Dep.target 设置为当前的作用域(步骤 5 中的代码)。这时候我们调用计算属性的 getter。如果我们的计算属性依赖了 data 数据或者 prop 数据,就会触发其对应的 getter 方法。这时候就会触发依赖收集。
    dep.depend()
    
    // 693
    Dep.prototype.depend = function depend() {
      if (Dep.target) {
        Dep.target.addDep(this)
      }
    }
    
    // 3169
    Watcher.prototype.addDep = function addDep(dep) {
      var id = dep.id
      if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id)
        this.newDeps.push(dep)
        if (!this.depIds.has(id)) {
          dep.addSub(this)
        }
      }
    }
    
    // 684
    Dep.prototype.addSub = function addSub(sub) {
      this.subs.push(sub)
    }
    
    
    • 9、我们在改变对应的依赖时候触发其 set 方法,dep.notify() 方法最终通知改变 dirty = true,从而重新计算 value
    dep.notify()
    
    // 699
    Dep.prototype.notify = function notify() {
      // stabilize the subscriber list first
      var subs = this.subs.slice()
      for (var i = 0, l = subs.length; i < l; i++) {
        subs[i].update()
      }
    }
    
    // 3200
    Watcher.prototype.update = function update() {
      /* istanbul ignore else */
      if (this.lazy) {
        this.dirty = true
      } else if (this.sync) {
        this.run()
      } else {
        queueWatcher(this)
      }
    }
    

    运行步骤

    • 初始化 cumputed 的时候会先初始化 props、data 等
    • 初始化 cumputed(!SSR) 时候,会添加其对应的 watcher(创建的时候不会触发 get 方法)。
    • 我们在获取计算属性的值的时候,如果 dirty = true 会调用 watcher.get 方法重新计算。随后 dirty = false
    • 在调用 get 方法的时候启动依赖收集,将 watcher 添加到对应依赖的 sub 里
    • 在依赖项发生变化的时候会调用 set 方法,最终会调用 watcher.update 方法将 dirty = true。这时候获取计算属性的时候会重新调用 watcher.get 方法

    总结

    • 我们在依赖项没有变化的时候,一直使用的是 watcher.value 也就是我们所说的缓存
    • 依赖项发生变化的时候重新计算一次 watcher.value

    补充

    • 10、我们在 computed 收集依赖完成之后,就会调用 cleanupDeps 方法,步骤 5。将 newDeps 中的依赖转存到 deps 中
    // 3176
    Watcher.prototype.cleanupDeps = function cleanupDeps () {
        var this$1 = this;
    
      var i = this.deps.length;
      while (i--) {
        var dep = this$1.deps[i];
        if (!this$1.newDepIds.has(dep.id)) {
          dep.removeSub(this$1);
        }
      }
      var tmp = this.depIds;
      this.depIds = this.newDepIds;
      this.newDepIds = tmp;
      this.newDepIds.clear();
      tmp = this.deps;
      this.deps = this.newDeps;
      this.newDeps = tmp;
      this.newDeps.length = 0;
    };
    
    // 689
    Dep.prototype.removeSub = function removeSub (sub) {
      remove(this.subs, sub);
    };
    
    • 11、我们通常一个 computed 会依赖另外一个 computed。假设 computedB 依赖了 computedA。当我们获取 computedB 的时候第一次调用 get。这时候(步骤 5) Dep.target === computedB -> computedB.getter -> computedA.getter 就会开启依赖收集,执行 computedA 的 watcher.depend()(步骤 4)。其中的 this$1.deps 就是 computedA 的依赖,这个方法将 computedB 的 watcher 添加到 computedA 的依赖中
    // 3254
    Watcher.prototype.depend = function depend () {
        var this$1 = this;
    
      var i = this.deps.length;
      while (i--) {
        this$1.deps[i].depend();
      }
    };
    
    • 12、这时候 computedA 的依赖添加了 computedA 的 watcher 和 computedB 的 watcher。computedB 的依赖添加 computedB 的 watcher。相关依赖触发了 set 会通知到对应的 computed watcher,从而触发重新计算
    • 13、当我们组件注销的时候同时会触发对应的注销函数,清除对应的 watcher
    // 2708
    if (vm._watcher) {
      vm._watcher.teardown();
    }
    var i = vm._watchers.length;
    while (i--) {
      vm._watchers[i].teardown();
    }
    
    // 3266
    Watcher.prototype.teardown = function teardown () {
        var this$1 = this;
    
      if (this.active) {
        // remove self from vm's watcher list
        // this is a somewhat expensive operation so we skip it
        // if the vm is being destroyed.
        if (!this.vm._isBeingDestroyed) {
          remove(this.vm._watchers, this);
        }
        var i = this.deps.length;
        while (i--) {
          this$1.deps[i].removeSub(this$1);
        }
        this.active = false;
      }
    };
    
    • 14、步骤9 中的 update 方法,在不同情况下会触发不同的方法,这里我们就不一一解释了。感兴趣的可以自己去看看
    展开全文
  • 虽然目前技术栈已由 Vue 转到了 React,但从之前使用 Vue 开发多个项目实际经历来看还是非常愉悦Vue 文档清晰规范,api 设计简洁高效,对前端开发人员友好,上手快,甚至个人认为在很多场景使用 Vue 比 ...
  • vue的computed实现原理---------需要建立数据依赖搜集,动态计算实现原理 1)问题:计算属性如何与属性建立依赖关系?属性发生变化又如何通知到计算属性重新计算? 如何建立依赖关系?----------利用 ...
  • Vue实现computed计算属性(Vue原理之步骤六) 基于上一篇(Vue实现数组) 根据计算属性几个特点设计思路: 1.他值是一个函数运行结果; 2.函数里用到所有属性都会引起计算属性变化; 计算属性仍然属于Vue响应式...
  • 今天主要是讨论 computed 实现原理computed在内部主要是运用 Watcher和 Dep构造函数进行收集依赖和派发更新。 咱们先来看看 Watcher和 Dep源码。 var uid = 0; /** * dep 就是用来给每...
  • ,我们通过实现一个简单版的和Vue中computed具有相同功能的函数来了解computed是如何工作的。对Vue.js中的computed工作原理感兴趣的朋友跟随脚本之家小编一起学习吧
  • 看这篇文章之前最好先对Vue的MVVM实现原理有一定的认识,因为这是Vue的核心概念,其他的工具大部分都是在此之上锦上添花,如果你不是很了解,可以先看看这篇文章: Vue的MVVM原理 实现原理分析 initState 在Vue源码...
  • computed实现原理

    2020-01-07 17:36:43
    computed实现原理 Vue响应系统 vue会遍历 data 中对象,通过使用 Object.defineProperty 将这些属性全部转为 getter/setter,在属性被访问和修改时通知变化,每个组件都有相应 watcher 实例对象,他会在组件...
  • 我们通过实现一个简单版Vuecomputed具有相同功能函数来了解computed是如何工作。 JS属性: JavaScript有一个特性是 Object.defineProperty ,它能做很多事,但我在这篇文章只专注于这个方法中一个: ...
  • 1、计算属性默认不执行 Object.defineProperty =>getter 只有在取值时候才执行 多次取值如果依赖值不变化就只执行一次,是缓存原因 依赖值变化了,需要重新执行...const vm = new Vue({ computed: { ...
  • 浅谈Vue中计算属性computed的实现原理

    千次阅读 2018-12-18 21:11:55
    虽然目前的技术栈已由Vue转到了React,但从之前...之前也有断断续续研读过Vue的源码,但一直没有梳理总结,所以在此做一些技术归纳同时也加深自己对Vue的理解,那么今天要写的便是Vue中最常用到的API之一computed...
  • 一个开始 有如下代码,full是一个计算属性,开始,他值是'hello world',1s后,msg变成了‘I...其原理解析来看一下。 <div id="app"> <span :msg="msg"></span> <div> {{full}}&l...
  • 1.Vuecomputed 和 watch区别。 computed 计算属性 : 依赖其它属性值,并且 computed 值有缓存,只有它依赖...2.computed 的实现原理 computed 本质是一个惰性求值观察者。 computed 内部实现了一个惰性 watch
  • 一、响应式的底层实现1、Vue与MVVMVue是一个 MVVM框架,其各层的对应关系如下View层:在Vue中是绑定dom对象的...View层会在ViewModel的作用下,实现自动更新2、Vue的响应式原理Vue响应式底层实现方法是 O...
  • computed里面数据,挂载在vm上即可实现 function Vue(options = {}) { this.$options = options;//将所有属性挂载在options var data = this._data = this.$options.data; observe(data) //观察完之后...
  • 我们通过实现一个简单版Vuecomputed具有相同功能函数来了解computed是如何工作。写十分全面细致,具有一定参考价值,对此有需要朋友可以参考学习下。如有不足之处,欢迎批评指正。 JS属性: ...
  • 我们通过实现一个简单版Vuecomputed具有相同功能函数来了解computed是如何工作。写十分全面细致,具有一定参考价值,对此有需要朋友可以参考学习下。如有不足之处,欢迎批评指正。 JS属性: ...
  • computed是一个计算属性,类似于过滤器,对绑定到view数据进行处理,计算属性结果会被缓存,除非依赖响应式属性变化才会重新计算。它用来监控自己定义变量,该变量不在data里面声明,直接在computed里面定义...
  • 两个月前我曾在掘金翻译了一篇关于Vue中简单介绍...Tips:如果你之前没有看过Vue的源码或者不太了解Vue数据绑定的原理的话,推荐你看我之前的一篇文章简单易懂的Vue数据绑定源码解读,或者其他论坛博客相关的文章都可以

空空如也

空空如也

1 2 3 4 5 ... 11
收藏数 201
精华内容 80
关键字:

vue的computed实现原理

vue 订阅