vue源码

01-vue源码学习-柯里化函数

一、概念:

柯里化: 一个函数原本有多个参数, 之传入一个参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构.
偏函数: 一个函数原本有多个参数, 之传入一部分参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构.
高阶函数: 一个函数参数是一个函数, 该函数对参数这个函数进行加工, 得到一个函数, 这个加工用的函数就是高阶函数.
为什么要使用科里化? 为了提升性能. 使用科里化可以缓存一部分能力.

二、代码实现

vue源码中判断是否是html标签函数

    let tags = 'div,p,a,img,ul,li'.split(',')
    function makeMap(tags) {
      let set = {}
      tags.forEach(tag => set[tag.toLowerCase()] = true)
      return function (tagname) {
        return !!set[tagname.toLowerCase()]
      }
    }

    let isHtmlTag = makeMap(tags)
    console.log(isHtmlTag);
    console.log(isHtmlTag('div'));

三、柯里化函数原理其实是闭包

闭包使用的利弊衡量:

使用闭包可能会存在内存泄露,或者性能会有些问题。但是,如果在具体的场景中,使用闭包能够提高性能,而且内存泄露风险并不高,其实是可以使用闭包的。开发过程中,很多时候的抉择,并不是绝对的,只有适合的方法或者架构,极少有完美的方法或者架构。

02-vue源码学习-vue的基本执行流程和简单渲染模型

一、vue的基本执行流程

获得模板: 模板中有 “坑”(比如{{}})
利用 Vue 构造函数中所提供的数据来 “填坑”, 得到可以在页面中显示的 “标签了”
将标签替换页面中原来有坑的标签
总的来说:Vue利用我们提供的数据和 页面中模板生成了一个新的 HTML 标签 ( node 元素 ),替换到了页面中放置模板的位置.

二、简单渲染模型

Vue 本质上是使用 HTML 的字符串作为模板的, 将字符串的 模板 转换为 AST(抽象语法树), 再转换为 VNode(虚拟dom).

  • 模板 -> AST
  • AST -> VNode
  • VNode -> DOM

那一个阶段最消耗性能?

最消耗性能是字符串解析 ( 模板 -> AST )

三、模仿vue的渲染流程

将真正的 DOM 转换为 虚拟 DOM
将虚拟 DOM 转换为 真正的 DOM

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id="root">
    <div class="c1">
      <div title="tt1" id="id">{{ name }}</div>
      <div title="tt2">{{ age }}</div>
      <div title="tt3">{{ gender }}</div>
      <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
      </ul>
    </div>
  </div>
  <script>
    /** 虚拟 DOM 构造函数 */
    class VNode {
      constructor( tag, data, value, type ) {
        this.tag = tag && tag.toLowerCase();
        this.data = data;
        this.value = value;
        this.type = type;
        this.children = [];
      }

      appendChild ( vnode ) {
        this.children.push( vnode );
      }
    }
    /** 由 HTML DOM -> VNode: 将这个函数当做 compiler 函数 */
    function getVNode( node ) { 
      let nodeType = node.nodeType;
      let _vnode = null;
      if ( nodeType === 1 ) {
        // 元素
        let nodeName = node.nodeName;
        let attrs = node.attributes;
        let _attrObj = {};
        for ( let i = 0; i < attrs.length; i++ ) { // attrs[ i ] 属性节点 ( nodeType == 2 )
          _attrObj[ attrs[ i ].nodeName ] = attrs[ i ].nodeValue;
        }
        _vnode = new VNode( nodeName, _attrObj, undefined, nodeType );

        // 考虑 node 的子元素
        let childNodes = node.childNodes;
        for ( let i = 0; i < childNodes.length; i++ ) {
          _vnode.appendChild( getVNode( childNodes[ i ] ) ); // 递归
        }

      } else if ( nodeType === 3 ) {

        _vnode = new VNode( undefined, undefined, node.nodeValue, nodeType );
      }

      return _vnode;
    }

    /** 将虚拟 DOM 转换成真正的 DOM */
    function parseVNode( vnode ) {
      // 创建 真实的 DOM
      let type = vnode.type;
      let _node = null;
      if ( type === 3 ) {
        return document.createTextNode( vnode.value ); // 创建文本节点
      } else if ( type === 1 ) {

        _node = document.createElement( vnode.tag );

        // 属性
        let data = vnode.data; // 现在这个 data 是键值对
        Object.keys( data ).forEach( ( key ) => {
          let attrName = key;
          let attrValue = data[ key ];
          _node.setAttribute( attrName, attrValue );
        } );

        // 子元素
        let children = vnode.children;
        children.forEach( subvnode => {
          _node.appendChild( parseVNode( subvnode ) ); // 递归转换子元素 ( 虚拟 DOM )
        } );

        return _node;
      }

    }

    let rkuohao = /\{\{(.+?)\}\}/g;
    /** 根据路径 访问对象成员 */
    function getValueByPath( obj, path ) {
      let paths = path.split( '.' ); // [ xxx, yyy, zzz ]
      let res = obj;
      let prop;
      while( prop = paths.shift() ) {
        res = res[ prop ];
      }
      return res;
    }
    /** 将 带有 坑的 Vnode 与数据 data 结合, 得到 填充数据的 VNode: 模拟 AST -> VNode */
    function combine( vnode, data ) {
      let _type = vnode.type;
      let _data = vnode.data;
      let _value = vnode.value;
      let _tag = vnode.tag;
      let _children = vnode.children;


      let _vnode = null;

      if ( _type === 3 ) { // 文本节点 

        // 对文本处理
        _value = _value.replace( rkuohao, function ( _, g ) {
          return getValueByPath( data, g.trim() );
        } );

        _vnode = new VNode( _tag, _data, _value, _type )

      } else if ( _type === 1 ) { // 元素节点
        _vnode = new VNode( _tag, _data, _value, _type );
        _children.forEach( _subvnode => _vnode.appendChild( combine( _subvnode, data ) ) );
      }

      return _vnode;
    }


    function JGVue( options ) {
      this._data = options.data;
      let elm = document.querySelector( options.el ); // vue 是字符串, 这里是 DOM 
      this._template = elm;
      this._parent = elm.parentNode;

      this.mount(); // 挂载
    }  

    JGVue.prototype.mount = function () {
      // 需要提供一个 render 方法: 生成 虚拟 DOM
      this.render = this.createRenderFn() // 带有缓存 ( Vue 本身是可以带有 render 成员 )

      this.mountComponent();
    }
    JGVue.prototype.mountComponent = function () {
      // 执行 mountComponent() 函数 
      let mount = () => { // 这里是一个函数, 函数的 this 默认是全局对象 "函数调用模式"
        this.update( this.render() )
      }
      mount.call( this ); // 本质应该交给 watcher 来调用, 但是还没有讲到这里

      // 为什么
      // this.update( this.render() ); // 使用发布订阅模式. 渲染和计算的行为应该交给 watcher 来完成
    }

    /**
     * 在真正的 Vue 中使用了 二次提交的 设计结构
     * 1. 在 页面中 的 DOM 和 虚拟 DOM 是一一对应的关系
     * 2. 先 有 AST 和 数据 生成 VNode ( 新, render )
     * 3. 将 就的 VNode 和 新的 VNode 比较 ( diff ), 更新 ( update )
     */

    // 这里是生成 render 函数, 目的是缓存 抽象语法树 ( 我们使用 虚拟 DOM 来模拟 )
    JGVue.prototype.createRenderFn = function () {
      let ast = getVNode( this._template );
      // Vue: 将 AST + data => VNode
      // 我们: 带有坑的 VNode + data => 含有数据的 VNode
      return function render () {
        // 将 带有 坑的 VNode 转换为 待数据的 VNode
        let _tmp = combine( ast, this._data );
        return _tmp;
      }
    }

    // 将虚拟 DOM 渲染到页面中: diff 算法就在里
    JGVue.prototype.update = function ( vnode ) {
      // 简化, 直接生成 HTML DOM replaceChild 到页面中
      // 父元素.replaceChild( 新元素, 旧元素 )
      let realDOM = parseVNode( vnode );

      // debugger;
      // let _ = 0;

      this._parent.replaceChild( realDOM, document.querySelector( '#root' ) );
      // 这个算法是不负责任的: 
      // 每次会将页面中的 DOM 全部替换
    }


    

    let app = new JGVue( {
      el: '#root',
      data: {
        name: '张三'
        , age: 19
        , gender: '难'
      }
    } );

  </script>
</body>
</html>

03-vue源码学习-响应式原理(对象和数组)

一、响应式原理

什么是响应式原理:

意思就是在改变数据的时候,视图会跟着更新。

实现响应式原理的核心:Object.defineProperty

Object.defineProperty( 对象, '设置什么属性名', {
  writeable
  configurable
  enumerable:  控制属性是否可枚举, 是不是可以被 for-in 取出来
  set() {}  赋值触发
  get() {}  取值触发
} )

二、对象响应式化

    <script>
        let o = {
            name: 'jim',
            age: 19,
            gender: '男'
        }

        // 简化后的版本
        function defineReactive(target, key, value, enumerable) {
            // 函数内部就是一个局部作用域, 这个 value 就只在函数内使用的变量 ( 闭包 )
            Object.defineProperty(target, key, {
                configurable: true,
                enumerable: !!enumerable,
                get() {
                    console.log(`读取 o 的 ${key} 属性`); // 额外
                    return value
                },
                set(newVal) {
                    console.log(`设置 o 的 ${key} 属性为: ${newVal}`); // 额外
                    value = newVal
                }
            })
        }
        let keys = Object.keys(o)
        keys.forEach(key => {
            defineReactive(o, key, o[key], true)
        })

    </script>

三、对象数组响应式化

    <script>
        let data = {
            name: '张三',
            age: 19,
            course: [
                { name: '语文' },
                { name: '数学' },
                { name: '英语' },
            ]
        }
        // 简化后的数据响应函数
        function defineReactive(target, key, value, enumerable) {
            // 函数内部就是一个局部作用域, 这个 value 就只在函数内使用的变量 ( 闭包 )
            if (typeof value === 'object' && value != null && !Array.isArray(value)) {
                // 是非数组的引用类型
                reactify(value); // 递归
            }
            Object.defineProperty(target, key, {
                configurable: true,
                enumerable: !!enumerable,
                get() {
                    console.log(`读取 o 的 ${key} 属性`); // 额外
                    return value
                },
                set(newVal) {
                    console.log(`设置 o 的 ${key} 属性为: ${newVal}`); // 额外
                    value = newVal
                }
            })
        }

        // 将对象 o 响应式化
        function reactify(o) {
            keys = Object.keys(o)
            for (let i = 0; i < keys.length; i++) {
                let key = keys[i]
                let value = o[key]
                // 判断这个属性是不是引用类型, 判断是不是数组
                // 如果引用类型就需要递归, 如果不是就不用递归
                //  如果不是引用类型, 需要使用 defineReactive 将其变成响应式的
                //  如果是引用类型, 还是需要调用 defineReactive 将其变成响应式的
                // 如果是数组呢? 就需要循数组, 然后将数组里面的元素进行响应式化
                if (Array.isArray(value)) {
                    for (let j = 0; j < value.length; j++) {
                        reactify(value[j])
                    }
                } else {
                    // 对象或值类型
                    defineReactive(o, key, value, true);
                }
            }
        }
        reactify(data)

    </script>

四、拦截数组方法

在改变数组时,加入的元素也要响应式化,所以要对数组方法进行重写,采用的方式是拦截数组的方法。

拦截数组方法:

<script>
        let ARRAY_METHOD = [
            'push',
            'pop',
            'shift',
            'unshift',
            'reverse',
            'sort',
            'splice',
        ];

        // 思路, 原型式继承: 修改原型链的结构
        let arr = [];
        // 继承关系: arr -> Array.prototype -> Object.prototype -> ...
        // 继承关系: arr -> 改写的方法 -> Array.prototype -> Object.prototype -> ...
        let array_methods = Object.create(Array.prototype)
        ARRAY_METHOD.forEach(method => {
            array_methods[method] = function () {
                // 调用原来的方法
                console.log('调用的是拦截的 ' + method + ' 方法');
                // 将数据进行响应式化
                let res = Array.prototype[method].apply(this, arguments)
                return res
            }
        })

        arr.__proto__ = array_methods
    </script>

五、完整的对象和数组响应式化代码

  <script>


    let data = {
      name: '张三',
      age: 19,
      course: [
        { name: '语文' },
        { name: '数学' },
        { name: '英语' },
      ]
    }; // 除了递归还可以使用队列 ( 深度优先转换为广度优先 )



    let ARRAY_METHOD = [
      'push',
      'pop',
      'shift',
      'unshift',
      'reverse',
      'sort',
      'splice',
    ];
    let array_methods = Object.create(Array.prototype);
    ARRAY_METHOD.forEach(method => {
      array_methods[method] = function () {
        // 调用原来的方法
        console.log('调用的是拦截的 ' + method + ' 方法');

        // 将数据进行响应式化
        for (let i = 0; i < arguments.length; i++) {
          reactify(arguments[i]);
        }

        let res = Array.prototype[method].apply(this, arguments);
        // Array.prototype[ method ].call( this, ...arguments ); // 类比
        return res;
      }
    });

    // 简化后的版本
    function defineReactive(target, key, value, enumerable) {
      // 函数内部就是一个局部作用域, 这个 value 就只在函数内使用的变量 ( 闭包 )

      if (typeof value === 'object' && value != null && !Array.isArray(value)) {
        // 是非数组的引用类型
        reactify(value); // 递归
      }

      Object.defineProperty(target, key, {
        configurable: true,
        enumerable: !!enumerable,

        get() {
          console.log(`读取 ${key} 属性`); // 额外
          return value;
        },
        set(newVal) {
          console.log(`设置 ${key} 属性为: ${newVal}`); // 额外
          value = newVal;
        }
      });
    }
    // 将对象 o 响应式化
    function reactify(o) {
      let keys = Object.keys(o);

      for (let i = 0; i < keys.length; i++) {
        let key = keys[i]; // 属性名
        let value = o[key];
        if (Array.isArray(value)) {
          // 数组
          value.__proto__ = array_methods; // 数组就响应式了
          for (let j = 0; j < value.length; j++) {
            reactify(value[j]); // 递归
          }
        } else {
          // 对象或值类型
          defineReactive(o, key, value, true);
        }
      }
    }


    reactify(data);


  </script>

04-vue源码学习-proxy

proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

app._data.name
// vue 设计, 不希望访问 _ 开头的数据
// vue 中有一个潜规则:
//  - _ 开头的数据是私有数据
//  - $ 开头的是只读数据
app.name
// 将 对 _data.xxx 的访问 交给了 实例

// 重点: 访问 app 的 xxx 就是在访问 app._data.xxx

假设:

var  o1 = { name: '张三' };
// 要有一个对象 o2, 在访问 o2.name 的时候想要访问的是 o1.name
Object.defineProperty( o2, 'name', {
  get() {
    return o1.name
  }
} );

访问 app 的 xxx 就是在访问 app._data.xxx

Object.defineProperty( app, 'name', {
  get() {
    return app._data.name
  },
  set( newVal ) {
    app._data.name = newVal;
  }
} )

将属性的操作转换为 参数

function proxy( app, key ) {
  Object.defineProperty( app, key, {
    get() {
      return app._data[ key ]
    },
    set( newVal ) {
      app._data[ key ] = newVal;
    }
  } )
}

问题:

在 vue 中不仅仅是只有 data 属性, properties 等等 都会挂载到 Vue 实例上

function proxy( app, prop, key ) {
  Object.defineProperty( app, key, {
    get() {
      return app[ prop ][ key ]
    },
    set( newVal ) {
      app[ prop ][ key ] = newVal;
    }
  } )
};

// 如果将 _data 的成员映射到 实例上
proxy( 实例, '_data', 属性名 )
// 如果要 _properties 的成员映射到 实例上
proxy( 实例, '_properties', 属性名 )

05-vue源码学习-发布订阅模式

发布订阅模式
目标: 解耦, 让各个模块之间没有紧密的联系

现在的处理办法是 属性在更新的 时候 调用 mountComponent 方法.

问题: mountComponent 更新的是什么??? (现在) 全部的页面 -> 当前虚拟 DOM 对应的页面 DOM

在 Vue 中, 整个的更新是按照组件为单位进行 判断, 已节点为单位进行更新.

  • 如果代码中没有自定义组件, 那么在比较算法的时候, 我们会将全部的模板 对应的 虚拟 DOM 进行比较.
  • 如果代码中含有自定义组件, 那么在比较算法的时候, 就会判断更新的是哪一些组件中的属性, 只会判断更新数据的组件, 其他组件不会更新.

复杂的页面是有很多组件构成. 每一个属性要更新的都要调用 更新的方法?

目标, 如果修改了什么属性, 就尽可能只更新这些属性对应的页面 DOM

这样就一定不能将更新的代码写死.

例子: 预售可能一个东西没有现货, 告诉老板, 如果东西到了 就告诉我.

老板就是发布者
订阅什么东西作为中间媒介
我就是订阅者

使用代码的结构来描述:

  1. 老板提供一个 账簿( 数组 )
  2. 我可以根据需求订阅我的商品( 老板要记录下 谁 定了什么东西, 在数组中存储 某些东西 )
  3. 等待, 可以做其他的事情
  4. 当货品来到的时候, 老板就查看 账簿, 挨个的打电话 ( 遍历数组, 取出数组的元素来使用 )

实际上就是事件模型

  1. 有一个 event 对象
  2. on, off, emit 方法

实现事件模型, 思考怎么用?

  1. event 是一个全局对象
  2. event.on( ‘事件名’, 处理函数 ), 订阅事件
    1. 事件可以连续订阅
    2. 可以移除: event.off()
      1. 移除所有
      2. 移除某一个类型的事件
      3. 移除某一个类型的某一个处理函数
  3. 写别的代码
  4. event.emit( ‘事件名’, 参数 ), 先前注册的事件处理函数就会依次调用

原因:

  1. 描述发布订阅模式
  2. 后面会使用到事件

发布订阅模式 ( 形式不局限于函数, 形式可以是对象等 ) :

  1. 中间的全局的容器, 用来存储可以被触发的东西( 函数, 对象 )
  2. 需要一个方法, 可以往容器中传入东西 ( 函数, 对象 )
  3. 需要一个方法, 可以将容器中的东西取出来使用( 函数调用, 对象的方法调用 ) 事件模型代码
<script>
        // 全局的 event 对象, 提供 on, off, emit 方法  
        var event = (function () {
            eventObjs = {}
            return {
                /** 注册事件, 可以连续注册, 可以注册多个事件 */
                on: function (type, handler) {
                    (eventObjs[type] || (eventObjs[type] = [])).push(handler)
                },
                /** 移除事件, 
                 * - 如果没有参数, 移除所有事件, 
                 * - 如果只带有 事件名 参数, 就移除这个事件名下的所有事件,
                 * - 如果带有 两个 参数, 那么就是表示移除某一个事件的具体处理函数
                 * */
                off: function (type, handler) {
                    if (arguments.length === 0) {
                        eventObjs = {}
                    } else if (arguments.length === 1) {
                        eventObjs[type] = 1
                    } else if (arguments.length === 2) {
                        let _events = eventObjs[type]
                        if (!_events) return
                        for (let i = _events.length; i >= 0; i--) {
                            if (_events[i] === handler) {
                                _events.splice(i, 1)
                            }
                        }
                    }
                },
                /** 
                 * 发射事件, 触发事件, 包装参数 传递给事件处理函数
                */
                emit: function (type) {
                    let args = Array.prototype.splice.call(arguments, 1)// 或 arguments 从 1 开始后的所有参数, 返回的是数组
                    let _events = eventObjs[type]
                    if (!_events) return
                    for (let i = 0; i < _events.length; i++) {
                        _events[i].apply(null, args)
                    }
                }
            }
        }())
    </script>
    <script>
        function f() { console.log(1) }
        function foo() { console.log(2) }


        // 注册事件
        event.on('click', () => console.log('第一个 click 事件')); // 无法移除
        event.on('click', () => console.log('第2个 click 事件'));
        event.on('click', () => console.log('第3个 click 事件'));
        event.on('click', () => console.log('第4个 click 事件'));
        event.on('click', () => console.log('第5个 click 事件'));

        console.log(1);
        console.log(1);
        console.log(1);
        console.log(1);
        console.log(1);
        console.log(1);


        function f() {
            event.emit('click');
        }

        // js 中 基本类型是比较值
        // 引用类型是 比较 地址
        // 引用类型与基本类型, 是将其转换为 基本类型再比较 , 如果是 === 严格等于是不转换比较
    </script>```


06-vue源码学习-理解依赖收集和派发更新(Observer、Watcher、Dep)

转载至:彻底理解Vue中的Watcher、Observer、Dep

在数据响应化时,在getter方法中做依赖收集,在setter方法中做派发更新。dep用于存储依赖和派发更新。

思考以下代码

new Vue({
  el: '#example',
  data(){
      return{
          obj:{
              a:1
          }
      }
  },
})

当我们写下这行代码时,vue将我们在data内定义的obj对象进行依赖追踪.

具体做法为执行new Observer(obj)

//经过上面的代码,我们的obj对象会变为以下的样子
{
  obj:{
    a:1,
    __ob__:{ //Observer 实例
        dep:{Dep 实例
            subs:[ //存放 Watcher 实例
              new Watcher(),
              new Watcher(),
              new Watcher(),
              new Watcher(),
            ]
        }
    }
  }
}


我们来一步步实现看下。

在obj对象上新增__ob__属性,值为Observe 类的实例,我们编写一个 def 函数,用来增加属性

function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  });
}

增加啥属性呢?之前提到了,我们需要增加一个 Observer 实例,实现如下

Observe 实现

const Dep = require('./Dep')

class Observer {
  constructor(targetObject) {
    def(targetObject, '__ob__', this);//在 targetObject 上 添加  Observer 实例, setter时 通知该实例
    this.walk(targetObject)
    this.dep = new Dep()
  }

  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    });
  }

}

function observe(data) {
  if (Object.prototype.toString.call(data) !== '[object Object]') {
    return
  }
  new Observer(data)
}

function defineReactive(obj, key, val) {
  observe(val)

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get');
      const ob = this.__ob__
      ob.dep.depend();
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('set');
      if (newVal === val) return
      val = newVal
      observe(newVal)
      const ob = this.__ob__
      ob.dep.notify();
    },

  })
}

function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  });
}


module.exports = Observer

这里面牵扯到了 Dep,我们也把Dep实现下

Dep

class Dep {
  constructor() {
    this.subs = [];
  }

  addSub(sub) {
    this.subs.push(sub);
  }

  depend() {
    this.subs.push(Dep.target);
  }

  notify() {
    for (let i = 0; i < this.subs.length; i++) {
      this.subs[i].fn();
    }
  }
}

Dep.target = null;

module.exports=Dep

Observer 类 主要做了以下事情

遍历 data 下的每一个属性,若是对象,则 执行 new Observer() ,在对象上新增__ob__属性,该属性的值为 Observer 的实例
劫持对象属性的变化,在 getter 的时候,拿到 Observer 实例的dep实例,执行dep.depend(),代码如下

  const ob = this.__ob__
  ob.dep.depend();

看下 dep.depend()做了些啥

this.subs.push(Dep.target)

Dep.target添加到 订阅数组内(this.subs)

也就是说,只要我们 Dep.target 赋值了,再执行 dep.depend(),那么该值就会被添加到 dep 的 subs 数组内,比如

实现自动添加依赖
这个时候该 Watcher出场了

Watcher 实现

const Dep = require("./Dep");

class Watcher {
  constructor(vm, exp, fn) {
    this.vm = vm;
    this.exp = exp;
    this.fn = fn;
    Dep.target = this; //将自己挂载到 Dep.target,调用 Dep.depend时会读取该变量
    this.vm[exp];
  }

  update() {
    //加入队列
  }
}

module.exports = Watcher;


根据一个小例子来理解 Watcher

const obj = {
  a: 1,
  b: {
    c: 2
  }
}

new Observer(obj)
new Watcher(obj, 'a', () => {
  console.log('Watcher 回调执行')
})
obj.a='222'

流程如下:

  1. 先观测 obj 对象(new Observer(obj)
  2. 实例化Watcher时,会执行Dep.target = this,然后执行this.vm[exp],也就是取一次值,那么会触发
    getter,将自身(Watcher实例)添加到dep的订阅者数组内
  3. 最后,改变数据时候,触发setter
 set: function reactiveSetter(newVal) {
      if (newVal === val) return
      val = newVal
      observe(newVal)
      const ob = this.__ob__
      ob.dep.notify();
    },

执行ob.dep.notify()

 notify() {
    for (let i = 0; i < this.subs.length; i++) {
      this.subs[i].fn()
    }

遍历 订阅者(subs)执行回调函数,整个流程结束

07-vue源码学习-vue的响应式原理(MVVM原理)

什么是MVVM
MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。
在这里插入图片描述
要实现一个mvvm的库,我们首先要理解清楚其实现的整体思路。先看看下图的流程:
在这里插入图片描述

1.实现compile,进行模板的编译,包括编译元素(指令)、编译文本等,达到初始化视图的目的,并且还需要绑定好更新函数;
2.实现Observe,监听所有的数据,并对变化数据发布通知;
3.实现watcher,作为一个中枢,接收到observe发来的通知,并执行compile中相应的更新方法。
4.结合上述方法,向外暴露mvvm方法。

最简化 VUE的响应式原理(转载)
转载:最简化 VUE的响应式原理
下图是vue官方的原理图:
总共分为三步骤:

1、init 阶段
VUE 的 data的属性都会被reactive化,也就是加上 setter/getter函数

function defineReactive(obj: Object, key: string, ...) {
    const dep = new Dep()

    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        ....
        dep.depend()
        return value
        ....
      },
      set: function reactiveSetter (newVal) {
        ...
        val = newVal
        dep.notify()
        ...
      }
    })
  }
  
  class Dep {
      static target: ?Watcher;
      subs: Array<Watcher>;

      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就是一个观察者类,每一个data的属性都会有一个dep对象。当getter调用的时候,去dep里注册函数,
至于注册了什么函数,我们等会再说。
setter的时候,就是去通知执行刚刚注册的函数。

2、mount 阶段:

mountComponent(vm: Component, el: ?Element, ...) {
    vm.$el = el

    ...

    updateComponent = () => {
      vm._update(vm._render(), ...)
    }

    new Watcher(vm, updateComponent, ...)
    ...
}

class Watcher {
  getter: Function;

  // 代码经过简化
  constructor(vm: Component, expOrFn: string | Function, ...) {
    ...
    this.getter = expOrFn
    Dep.target = this                      // 注意这里将当前的Watcher赋值给了Dep.target
    this.value = this.getter.call(vm, vm)  // 调用组件的更新函数
    ...
  }
}

mount 阶段的时候,会创建一个Watcher类的对象。这个Watcher实际上是连接Vue组件与Dep的桥梁。
每一个Watcher对应一个vue component。

这里可以看出new Watcher的时候,constructor 里的this.getter.call(vm, vm)函数会被执行。getter就是updateComponent。这个函数会调用组件的render函数来更新重新渲染。

而render函数里,会访问data的属性,比如

render: function (createElement) {
  return createElement('h1', this.blogTitle)
}

此时会去调用这个属性blogTitle的getter函数,即:

// getter函数
get: function reactiveGetter () {
    ....
    dep.depend()
    return value
    ....
 },

// dep的depend函数
depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
}

在depend的函数里,Dep.target就是watcher本身(我们在class Watch里讲过,不记得可以往上第三段代码),这里做的事情就是给blogTitle注册了Watcher这个对象。这样每次render一个vue 组件的时候,如果这个组件用到了blogTitle,那么这个组件相对应的Watcher对象都会被注册到blogTitle的Dep中。

这个过程就叫做依赖收集。

收集完所有依赖blogTitle属性的组件所对应的Watcher之后,当它发生改变的时候,就会去通知Watcher更新关联的组件。

3、更新阶段:
当blogTitle 发生改变的时候,就去调用Dep的notify函数,然后通知所有的Watcher调用update函数更新。

notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
}

可以用一张图来表示:
在这里插入图片描述

由此图我们可以看出Watcher是连接VUE component 跟 data属性的桥梁。

08-自我总结的vue响应式原理

在这里插入图片描述

vue是采用数据劫持配合发布者-订阅者模式的方式,通过Object.definerProperty()来劫持各个属性的setter和getter,在数据变动时,发布消息给依赖收集器,去通过观察者,做出对应的回调函数,去更新试图。
MVVM作为绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听model数据变化,通过Compile来解析编译指令,最终利用Watcher搭起Observer,Compile之间的通信桥梁,达到数据变化=>视图更新;视图交互变化=>数据model变更的双向绑定效果。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值