学习Vue源码(三) 响应式原理


一、代码结构上的一些注意点

(二)中,render()函数已被实现,但一些地方尚未说明,现在先来扫个尾。

首先,回顾一下代码结构:

function LnVue( options ) {
  this._el = options.el;
  this._data = options.data;
  this._template = document.querySelector(this._el);

  this.mount() //挂载
}
LnVue.prototype.mount = function () {
 //提供一个render方法,生成虚拟DOM
  this.render = this.createRenderFn();
  this.mountComponent();
}

LnVue.prototype.mountComponent = function () {
  let mount = () => {
    this.update(this.render());
  }
  mount.call(this);
}
LnVue.prototype.createRenderFn = function () {
  /** 将AST与data合成生成VNode
  *  这里用带“坑”的VNode来模拟AST
     带“坑”的VNode + data = VNode */
  let ast = createVNode( this._template );
  return function render() {
    //将带坑的VNode转换为带数据的VNode(模拟)
    //真实Vue中render:AST+Data=VNode
    return combine(ast ,this._data);
  }
}

//将虚拟Dom渲染到页面上:Diff算法
LnVue.prototype.update = function () {

}

这里有几个注意点:

(1)为什么render()函数定义在mount中,而不是直接定义在prototype中?

在Vue中,render()函数是被设计成可以由使用者自己去提供的,即我们在代码中可能会这样写:

new Vue({
  el:'#root',
  render:(createElement) => {
    //
  }
})

那么此时,render()函数返回的结果将替换模板。
而在考虑到render可以被使用者提供的情况下,代码可能需要有一些改动:

function LnVue( options ) {
  this.$options = options;
  this._el = options.el;
  this._data = options.data;
  this._template = document.querySelector(this._el);

  this.mount() //挂载
}
LnVue.prototype.mount = function () {
  if(typeof this.$options.render === 'function'){
  	//
  }
  this.render = this.createRenderFn();
  this.mountComponent();
}

但是,render()被使用者自己提供的情况极少,这里为了简化代码起见,将这个判断删减了。

(2)在mountComponent()方法中,为什么不直接调用this.update,而是反回一个mount,调用mount?
即为什么不这样写?

LnVue.prototype.mountComponent = function () {
  this.update(this.render());
}

这是因为Vue在这个地方用到了发布订阅模式,所有有关渲染与计算的行为都应该由watcher来完成,而不是直接在mountComponent()实现。因此将该行为封装成一个mount函数,方便将它传递出去。

(3)update()函数

function LnVue( options ) {
  this._el = options.el;
  this._data = options.data;
  this.elm = this._template = document.querySelector(this._el);
  this._parent = this.elm.parentNode;

  this.mount() //挂载
}
LnVue.prototype.update = function ( vnode ) {
  //Vue中需要diff算法比对新旧DOM,这里简化
  //直接生成HTML到页面中
  let realDOM = parseVNode( vnode );
  this._parent.replaceChild(realDOM, this._template);
}

二、用Object.defineProperty实现响应式

用法详解

Object.defineProperty不再多说,具体用法可以参考上面的连接。

接下来主要说一下如何用该方法实现响应式。

在Vue内部,数据的响应式也是主要通过该方法实现的,Object.defineProperty主要用于定义一个属性,看两个例子:

Object.defineProperty(obj, "key", {
  enumerable: false,
  configurable: false,
  writable: false,
  value: "static"
});
var bValue = 38;
Object.defineProperty(o, "b", {
  get() { return bValue; },
  set(newValue) { bValue = newValue; },
  enumerable : true,
  configurable : true
});

上面两个例子展示了Object.defineProperty的两种用法,这里我们主要关注第二个例子,在该用法中,Object.defineProperty可以定义setter与getter用于在属性被修改或者获取时触发某种函数,这也正是实现响应式的关键所在,我们可以在setter中加入每当修改值时就重新渲染DOM的方法。

但是现在还有一个问题就是,setter与getter要想正常工作,还需要一个第三方变量来存储值,比如例子中的bValue。

但是我们显然不能在项目中大量定义这种全局变量,对于每一个属性,都定义一个全局变量,这显然是不行的。

Vue的处理策略是使用闭包的方法,将这个第三方中介值通过参数传递到Object.defineProperty中,这样就会成为Object.defineProperty的局部变量。

具体方法如下:

function defineReactive(target, prop, value, enumerable){
  Object.defineProperty(target, prop,{
    configurable:true,
    enumerable:!!enumerable,
    set( newVal ){
      value = newVal;
    },
    get(){
      return value;
    }
  })
}

同时我们还需要写一个递归处理数据的函数,因为在Vue中,数据是放在data属性中的,即将data中的所有数据变成响应式的。

function reactify( obj ) {
  let keys = Object.keys( obj ); //获得所有属性
  for(let i = 0; i < keys.length; i++){
    let key = keys[i];
    let value = obj[key];
    if( Array.isArray(value) ){ //如果值为数组对象就递归
      for(let j = 0; j < value.length; j++){
        reactify(value[j]);
      }
    }
    else { //如果是值类型或者对象类型直接设为响应式的
      defineReactive(obj, key, value, true);
    }
  }
}

上述方法还是存在着一些问题的,尽管data中的数据都变成响应式的了,但是你会发现,当我们通过push()等方法加入新数据时,新加入的数据不是响应式的,这显然需要改进。
在Vue中,可以依旧保持响应特性的方法有:push、pop、shift、unshift、reverse、splice、sort。

具体的思路是这样的:如果要在使用这些方法的时候依然保持响应式,那么在使用它们时做一些额外的操作就行了,关键点有二,一是额外的操作怎么写?这个额外的操作肯定是让新数据保持响应式的操作,这个稍后再说,第二个关键点就是我们怎么在使用这些数组方法时还能干其额外的事情?
这里容易想到的是重写这些方法,这个要在数组的原型链上改动一下:

arr -> Array.prototype -> Object.prototype
arr -> ??? -> Array.prototype -> Object.prototype

显然,如果我们在数组与Array.prototype之间再插入一个继承关系,那么在使用push等方法时就会优先在新插入的原型上找,就可以实现我们想要的操作。

//定义支持响应式的方法列表
let ARRAY_METHODS = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];
//定义待插入的原型,它继承自Array.prototype
let array_methods = Object.create(Array.prototype);

接下来就是在使用这些方法时先拦截下来,然后进行我们想要的操作:

//对所有方法遍历执行
ARRAY_METHODS.forEach( method => {
  array_methods[ method ] = function () {
    for(let i = 0; i < arguments.length; i++){
      //如果加入的数据是值类型
      if(typeof arguments[i] === "string" || typeof arguments[i] === 'number'
        || typeof arguments[i] === 'boolean'){
        defineReactive(this,'',arguments[i],true); //类似于 arr.push(666), 无属性所以设为''
      }
      //加入的是对象等引用类型
      else {
        reactify(arguments[i]); 
      }
    }
    return Array.prototype[ method ].apply(this, arguments); //调用Array.prototype上的相应方法返回,这里注意需要继承上下文,即apply
  }
})

然后在哪里使用呢?显然我们这个操作是针对数组的,即在这里改动:

function reactify( obj ) {
  let keys = Object.keys( obj ); //获得所有属性
  for(let i = 0; i < keys.length; i++){
    let key = keys[i];
    let value = obj[key];
    if( Array.isArray(value) ){ //如果值为数组对象就递归
      value.__proto__ = array_methods;  //新添加,继承我们新写的array_methods
      for(let j = 0; j < value.length; j++){
        reactify(value[j]);
      }
    }
    else { //如果是值类型直接设为响应式的
      defineReactive(obj, key, value, true);
    }
  }
}

至此,简化版的响应式代码就算差不多了,全部代码:

let data = {
  name:'小红帽',
  message:'喜欢采蘑菇',
  profile:{
    grade:'三年级',
    friends:{
      name:'大灰狼',
      grade:'二年级'
    }
  },
  course:[
    {'Chinese':99},
    {'Math':89},
    {'English':95}
  ]
}

let ARRAY_METHODS = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];
let array_methods = Object.create(Array.prototype);

function defineReactive(target, prop, value, enumerable){
  if(typeof value === "object" && value !== null && !Array.isArray(value)){
    reactify(value);
  }
  Object.defineProperty(target, prop,{
    configurable:true,
    enumerable:!!enumerable,
    set( newVal ){
      value = newVal;
    },
    get(){
      return value;
    }
  })
}

function reactify( obj ) {
  let keys = Object.keys( obj ); //获得所有属性
  for(let i = 0; i < keys.length; i++){
    let key = keys[i];
    let value = obj[key];
    if( Array.isArray(value) ){ //如果值为数组对象就递归
      value.__proto__ = array_methods;
      for(let j = 0; j < value.length; j++){
        reactify(value[j]);
      }
    }
    else { //如果是值类型直接设为响应式的
      defineReactive(obj, key, value, true);
    }
  }
}

ARRAY_METHODS.forEach( method => {
  array_methods[ method ] = function () {
    for(let i = 0; i < arguments.length; i++){
      //如果加入的数据是值类型
      if(typeof arguments[i] === "string" || typeof arguments[i] === 'number'
        || typeof arguments[i] === 'boolean'){
        defineReactive(this,'',arguments[i],true);
      }
      //加入的是对象等引用类型
      else {
        reactify(arguments[i]);
      }
    }
    return Array.prototype[ method ].apply(this, arguments);
  }
})

reactify(data);

接下来只需要把以上代码整合到我们之前写的LnVue项目中,在LnVue的构造函数中将数据reactify()一下,即可将data中的数据实现响应式。然后就是一旦数据发生改变渲染我们的页面,这里Vue中使用了发布订阅模式,这里就先略过,如果想试一下效果可以先向defineReactive()传入LnVue的实例,在调用setter时调用LnVue的mountComponent()即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值