当前位置 : 主页 > 编程语言 > 其它开发 >

Vue2手写源码

来源:互联网 收集:自由互联 发布时间:2022-05-22
响应式数据变化 数据发生变化后,我们可以监听到这个数据的变化 (每一步后面的括号是表示在那个模块进行的操作) 手写简单的响应式数据的实现(对象属性劫持、深度属性劫持、数
响应式数据变化

数据发生变化后,我们可以监听到这个数据的变化 (每一步后面的括号是表示在那个模块进行的操作)

 

手写简单的响应式数据的实现(对象属性劫持、深度属性劫持、数组函数劫持)、模板转成 ast 语法树、将 ast 语法树转换成 render 函数、render 函数生成虚拟节点、根据生成的虚拟节点创造真实 DOM、实现依赖收集

 

响应式数据的实现
  • 创建一个Vue实例 vm (index.html)

        const vm = new Vue({
         data() {
           return {
             name : 'zs',
             age : 18
          }
        }
      })
  • 在 index.js 中使用 this._init 做数据的初始化,并使用 initMaxin 方法,将 vue 实例传过去 (index.js)

    function Vue(options) {  // options 就是用户的选项
     this._init(options); //默认调用了init
    }
    initMixin(Vue); // 扩展了 init 方法
  • 在 init.js 中,将用户的数据信息挂载到 vue 实例上,并初始化状态 (init.js)

export function initMixin(Vue) { //就是给 Vue 增加 init 方法
 Vue.prototype._init = function (options) { //用于初始化操作
   //vue vm.$options 就是获取用户的配置

   // 将 用户的选项 挂载到 Vue 实例上
   const vm = this;
   vm.$options = options;

   // 初始化状态
   initState(vm)
}
}
  • 在 state.js 中,要先获取到所有的配置,判断是否存在某一配置,然进行获取这个配置。将data放到实例上的_data 上,然后对数据进行劫持。 (state.js)

export function initState(vm) {
 const opts = vm.$options;  //获取所有的选项
 if(opts.data) { //判断是否存在data
   initData(vm);
}
}

function initData(vm) {
 let data = vm.$options.data; //获取到data中的数据 data 可能是函数 也可能是对象

 // 判断 data 中数据是函数还是对象
 data = typeof data === 'function'? data.call(vm) : data;  //data 是用户返回的对象
 
 vm._data = data;  //将返回的对象放到 _data 上
 // 对数据进行劫持 vue2使用一个 api defineProperty
 observe(data);
}
  • 创建一个 处理 数据劫持的文件夹,在里面创建一个 index.js 文件进行数据劫持 (/observe/index.js)

  • 先判断数据是否为对象(只对对象进行劫持),再判断是否被劫持过(被劫持过就不需要再被劫持),再通过类实例进行劫持。 (/observe/index.js)

// 判断是否为对象
 if(typeof data !== 'object' || data == null) {
   return; //不是对象,直接 return 出来
}

 // 判断是否被劫持过,使用实例进行判断,被劫持过就不需要再被劫持
 return new Observe(data);
  • 声明一个类实例 Observe,然后再将 defineReactive 作为公共 API 导出 (/observe/index.js)

 class Observe {
 constructor(data) { //用户传入的数据
   this.walk(data)
}
 walk(data) { //循环这个 数据对象,对属性依次进行劫持

   // 重新定义属性 所以 vue2 性能会比 vue3 差
   Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
}
}

export function defineReactive(target,key,value) { //作为公共 API 导出,target:需要重新定义的   值,key:需要重新定义的值的key值,value:需要重新定义的值的value值
 observe(value); //值如果是对象的话,就再次进行劫持(避免深层次的值没有被劫持)
 Object.defineProperty(target, key, {
   get() {  //取值时候,执行 get
     console.log('get');
     return value;
  },
   set(newValue) {  //修改的时候,执行set
     console.log('set');
     if(value === newValue) return
     value = newValue;
  }
})
}
  • 访问 vm.name 就相当于访问 vm._data.name(还是在 state.js 中进行代理) (state.js)

  // 将vm._data 用 vm来进行代理   访问vm.name 就相当于 访问 vm._data.name
 for(let key in data) {
   proxy(vm, '_data', key)
}
 
 function proxy(vm, target, key) {
Object.defineProperty(vm, key, {
   get() {
     return vm[target][key];  
  },
   set(newValue) {
     vm[target][key] = newValue;
  }
})
}
  • 对于数组的话,会遍历数组内部的所有元素,造成性能的浪费。所以要对数组的方法进行重写。数组有七个方法可以修改本身。(/observe/index.js)

    Object.defineProperty(data, '__ob__', { //将 Observe 实例赋给data 对象的自定义属性上,让array.js中能拿到 Observe 中的方法。
     value : this,
     enumerable : false //将 __ob__ 变成不可枚举的(循环的时候就获取不到了)
  })
// 判断数据是不是数组
   if(Array.isArray(data)) {

     // 重写数组中的方法 7个方法 可以修改数组本身
     data.__proto__ = newArrayProto;

     // 在对数组里面的每一项进行劫持
     this.observeArray(data);
  } else {
     this.walk(data)  //不是数组就直接进入循环,进行数据劫持
  }
   
   observeArray(data) { //循环这个数组,然后进行数据劫持
  data.forEach(item => observe(item));
}
   
  • 在 array.js 中,重写了能修改数组本身的七个方法,还要对数组新增的数据再次进行劫持.(/observe/array.js)

// 获取数组的原型
let oldArrayProto = Array.prototype;

// newArrayProto._proto = oldArrayProto
export let newArrayProto = Object.create(oldArrayProto);

let methods = [ //找到所有的变异方法
 'push',
 'pop',
 'shift',
 'unshift',
 'reverse',
 'sort',
 'splice'
]

methods.forEach(method => {
 newArrayProto[method] = function(...args) {  //重写了数组的方法
   const result = oldArrayProto[method].call(this, ...args); //函数的劫持   切片编程


   console.log('method:' ,method);
   // 对新增的数据 也要再次进行劫持
   let inserted;
   let ob = this.__ob__;
   switch (method) {
     case 'push':
     case 'unshift':
       inserted = args;
       break;
     case 'splice':
       inserted = args.slice(2);
     default:
       break;
  }
   if(inserted) { //对新增的内容再次进行观测
     ob.observeArray(inserted); //使用观测实例中的 observeArray 方法,来遍历新增的数据,然后进行数据劫持。
  }

   return result;
}
})
  • 监听到数据变化后,要进行模板参数的解析

  • 要先判断有没有传进 el 参数 (init.js)

// 判断是否有传入 el 参数
   if(options.el) {
     vm.$mount(options.el); //实现数据的挂载
  }
  • 编写 $mount 方法 (init.js)

Vue.prototype.$mount = function (el) {
   const vm = this;
   el = document.querySelector(el)
   let ops = vm.$options
   if(!ops.render) { //判断是否写了 render
     let template;  //没有render 就看看有没有模板
     if(!ops.template && el) { //没有模板,但是有el
       template = el.outerHTML
    } else {
         if (el) {
           template = ops.template; //如果有el , 则采用模板的内容
        }
      }
     // 如果写了模板
     if(template) {
       const render = compileToFunction(template); //将模板编译成 render 函数
       ops.render = render;
    }
  }
   ops.render;
}

 

模板转化为 ast 语法树
  • compileToFunction 对模板进行编译处理(/compile/index.js)

    先将模板转化为 ast 语法树 (/compile/parse.js)

    • 利用正则先匹配 标签名、属性、文本内容

    • //利用正则匹配出模板
      const ncname = `[a-zA-Z][\\-\\.0-9_a-zA-Z]*`
      const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
      const startTagOpen = new RegExp(`^<${qnameCapture}`); //这里匹配的是开始标签名 <div>
      const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); //这里匹配的是结束标签名 </div>

      const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;  // 匹配属性的

      const startTagClose = /^\s*(\/?)>/; //匹配开始标签的两种格式 <div>   <div />

      const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;   // 匹配双花括号 {{}}
    • 然后依据这个正则开始匹配 模板中的内容,匹配到一个,就删除一个 (开始标签解析比较复杂,所以放在 parseStartTag 方法中)

    • // 存在 html 的时候
       while(html) {
         // 如果 textEnd 为 0,则说明是开始标签和结束标签。大于 0 就是文本的结束位置
         let textEnd = html.indexOf('<');
         debugger;
         // 匹配开始标签
         if(textEnd == 0) {
           const startTagMatch = parseStartTag(); //开始标签的匹配结果

           if(startTagMatch) { //解析到的开始标签
             start(startTagMatch.tagName, startTagMatch.attrs)
             continue;
          }
           
           // 如果不是开始标签,那就是结束标签
           let endTagMatch = html.match(endTag);
           if(endTagMatch) {
             advance(endTagMatch[0].length);
             end(endTagMatch.tagName)
             continue;
          }
        }
         // 匹配文本
         if(textEnd > 0) {
           let text = html.substring(0, textEnd); //截取的文本内容
           if(text) { //存在文本的话
             chars(text);
             advance(text.length) //删除文本的部分
          }
        }
      }
    • 开始的标签比较复杂,需要匹配开始的标签和标签中的属性

    • // 匹配开始标签
       function parseStartTag() {
         const start = html.match(startTagOpen);
         if(start) {
           const match = {
             tagName:start[1], //标签名
             attrs:[] //存放属性
          }
           advance(start[0].length); //匹配完开始标签后就去掉开始标签
           
           // 如果不是 开始标签的结束,就一直匹配下去
           let attr, end;
           while(!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { //如果没有匹配到结束标签的时候,就一直匹配下去
             advance(attr[0].length); //匹配完属性后就去掉属性
             match.attrs.push({name : attr[1], value : attr[3] || attr[4] || attr[5] || true}) //往 match 的 attrs 中存放属性的键值对
          }
           if(end) { //如果匹配到结束标签
             advance(end[0].length) //直接去掉结束标签
          }
           return match; //返回解析完成的结果
        }
         return false;  //不是标签
      }
    • 拿获取到的开始标签、文本、结束标签来做处理---最终转换成一颗抽象树

    • 抽象树的格式

        const ELEMENT_TYPE = 1; //元素类型
       const TEXT_TYPE = 3;//文本类型
       const stack = []; //创建用于存放元素的栈---利用栈先进后出的特性来构建抽象语法树
       let currentParent; //用于指向栈中的最后一个元素
       let root;//根节点

       // 转换为一颗抽象语法树
       function createASTElement(tag,attrs) {
         return {
           tag,
           type:ELEMENT_TYPE,
           children:[],
           attrs,
           parent:null
        }
      }
    •     // 对开始 文本 结束标签做处理---最终转换成一颗抽象语法树
        function start(tag, attrs) { //标签名 + 属性
         let node = createASTElement(tag,attrs); //调用 createASTElement 函数,生成一个 ast 节点
         if(!root) { //判断是否有根节点,没有的话就将当前节点作为根节点
           root = node;
        }
         if(currentParent) { //如果栈中已经有最后一个元素,则当前节点的父亲就是栈中最后一个元素
           node.parent = currentParent;
           currentParent.children.push(node);//还需要在父节点添加children属性的值
        }
         stack.push(node);//将节点放入栈中
         currentParent = node; //指向栈中的最后一个元素
      }
       function chars(text) { //文本内容直接放到当前指向的节点
         text = text.replace(/\s/g,'')
         text && currentParent.children.push({
           type:TEXT_TYPE,
           text,
           parent: currentParent
        })
      }
       function end() { //标签
         stack.pop(); //弹出 栈中最后一个元素
         currentParent = stack[stack.length - 1]; //更新 currentParent , 指向最后一个元素
      }

       

ast 语法树 转换成 render 方法

(/compiler/index.js)

// 2.生成 render方法 (render方法执行返回的结果就是 虚拟DOM)

 let code = codegen(ast);
 code = `with(this){return ${code}}`
 let render = new Function(code); //根据代码生成 render 函数
 
 return render;
   ```javascript
  // 转化为 render
  function codegen(ast) {
    let children = genChildren(ast.children)
    let code = `_c('${ast.tag}', ${ast.attrs.length > 0? genProps(ast.attrs) : 'null'}${ast.children.length ? `,${children}` : ''})`
    return code
  }
  ```
  • 转换的规则(属性的转换、文本内容的转换) (/compiler/index.js)

// 属性的转化
function genProps(attrs) {
 let str = ''
 for(let i = 0; i < attrs.length; i++) {
   let attr = attrs[i];
   if(attr.name === 'style') { //判断属性是不是style
     let obj = {};
     attr.value.split(';').forEach(item => {  //先利用 ; 进行分隔出成对的 key value。 再利用 : 分隔出单个的 key 和 value
       let [key, value] = item.split(':');
       obj[key] = value; //将成对的 key value 写入空对象中
    });
     attr.value = obj; //再将 obj 对象作为 value 放进 attr 中
  }
   str += `${attr.name}:${JSON.stringify(attr.value)},` //拼接属性
}
 return `{${str.slice(0, -1)}}`
}

// 生成元素和文本内容(普通文本内容,双花括号文本内容)
function gen(node) {
 if(node.type === 1) { //元素
   return codegen(node);
} else { //文本
   let text = node.text
   if(!defaultTagRE.test(text)) { //普通文本的话
     return `_v(${JSON.stringify(text)})`
  } else {
     // _v(_s(name) + 'one' + _s(name))
     let tokens = [];
     let match;
     defaultTagRE.lastIndex = 0;
     let lastIndex = 0; //最后匹配的位置
     while(match = defaultTagRE.exec(text)) {
       let index = match.index; //双花括号内容匹配的位置
       if(index > lastIndex) { //判断双花括号内容匹配的位置 看看是否中间有普通文本内容
         tokens.push(JSON.stringify(text.slice(lastIndex, index))); //有就将普通文本放进 tokens 中
      }
       tokens.push(`_s(${match[1].trim()})`) //将花括号内容放进 tokens
       lastIndex = index + match[0].length //更改最后匹配的位置
    }
     if(lastIndex < text.length) { //要是普通文本在 双花括号后面的话
       tokens.push(JSON.stringify(text.slice(lastIndex)))
    }
     return `_v(${tokens.join('+')})`  //将内容 return 出去
  }
}
}
// 孩子的转化
function genChildren(children) {
 return children.map(child => gen(child)).join(',')
}

 

根据render方法产生虚拟节点
  • 在 mountComponent 函数中 调用两个原型方法 (lifecycle.js)

export function mountComponent(vm, el) {
 vm.$el = el;
 
 // 1.调用 render 方法产生虚拟节点 虚拟DOM
 
 // 2.根据虚拟 DOM 生成真实 DOM

 // 3.插入到 el 元素中
 
 vm._update(vm._render());
}
  • 将 render 方法生成的 _c 、__v 、 _s 进行解析(lifecycle.js)

// _c  里面放标签、属性、孩子
 Vue.prototype._c = function() {
   return createElementVNode(this, ...arguments)
}
 Vue.prototype._s = function (value) {
   if(typeof value !== 'object') return value;
   return JSON.stringify(value)
}
 Vue.prototype._v = function () {
   return createTextVNode(this, ...arguments)
}
  • _c 中的 createElementVNode 方法 和 _v 中的 createTextVNode 方法 (/vdom/index.js)

// h() _c()
export function createElementVNode(vm, tag, data, ...children) {
 if(data == null) {
   data = {}
}
 let key = data.key;
 if(key) {
   delete data.key;
}
 return Vnode(vm,tag,key,data,children,undefined)
}

// _v()
export function createTextVNode(vm, text) {
 return Vnode(vm,undefined,undefined,undefined,undefined,text)
}

// ast 做的是语法层面上的转换 描述的是语法本身(html js css)
// 虚拟DOM 是描述 Dom 元素,可以增加一些自定义属性 (描述 DOM 的)
function Vnode(vm, tag, key, data, children, text) {
 return {
   vm,
   tag,
   key,
   data,
   children,
   text
}
}
  • _render 方法 (lifecycle.js)

Vue.prototype._render = function () {
   const vm = this;
   // 当渲染的时候,会去实例中取值,我们就可以将属性和视图绑定在一起了
   // call => 让 with 中的 this 指向 vm
   return vm.$options.render.call(vm); //调用 ast 语法树转义后生成的 render 方法
}

 

将虚拟节点转化为真实DOM
  • _update 方法 (lifecycle.js)

Vue.prototype._update = function (Vnode) {//将 Vnode 转化为真实 Dom
   const vm = this;
   const el = vm.$el;

   // patch既有初始化功能、又有更新功能
   vm.$el = patch(el, Vnode);
}
  • _update 的 patch 方法 --- 根据虚拟节点创建真实 DOM,将新节点放在老节点下面,然后移除老节点。

// 根据虚拟节点创建真实 DOM
function patch(oldVnode,Vnode) {
 // 初渲染流程
 const isRealElement = oldVnode.nodeType; //判断是 真实元素 还是 虚拟节点
 if(isRealElement) {
   const el = oldVnode; //获取真实元素
   const parentElm = el.parentNode; //获取父元素

   let newElm = createElm(Vnode); //根据虚拟节点创建真实 DOM
   parentElm.insertBefore(newElm, el.nextSibling); //将虚拟节点生成的真实节点放进老节点的下面
   parentElm.removeChild(el); //移除老节点

   return newElm
} else {
   // diff 算法
}
}
  • patch 中的 createElm 方法 --- 根据 render 方法的数据,创建出虚拟节点

// 根据 render 方法的数据 创建出虚拟节点
function createElm(Vnode) {
 let {tag, data, children, text} = Vnode;
 if(typeof tag === 'string') { //说明是标签,处理标签
   Vnode.el = document.createElement(tag); //将真实节点与虚拟节点对应起来,后续如果修改属性,(diff)就可以直接找到虚拟节点对应的真实节点
   
   patchProps(Vnode.el, data);  //标签的属性


   children.forEach(child => {   //处理儿子元素
     Vnode.el.appendChild( createElm(child));
  })

} else{ // 说明是文本,处理文本
   Vnode.el = document.createTextNode(text);
}
 return Vnode.el
}
  • createElm 中的 patchProps 方法 ---- 处理标签的属性

// 处理标签的属性
function patchProps(el, props) {
 for(let key in props) {
   if(key === 'style') { //属性是 style 的话
     for(let styleName in props.style) {
       el.style[styleName] = props.style[styleName];
    }
  } else { //普通属性的话,直接加进 el 就行
     el.setAttribute(key, props[key]);
  }
}
}

 

实现依赖收集

依赖收集实现原理简单来说:就是一个简单的观察者模式。先给每个属性 增加一个 dep(被观察者) ,然后通过 watcher(观察者)来观察。如果那个属性发生变化,就会通知观察者来更新

下面是 dep.js 和 watcher.js 模块的整体代码。建议先看一遍整体代码,再看一遍一步一步的解析,然后再看一遍整体代码。这样食用效果更佳哦~

dep.js


let id = 0;
class Dep {
 constructor() {
   this.id = id++;  //属性的 dep 要收集 watcher
   this.subs = [];  // 存放着当前属性对应的 watcher
}
 depend() {
   Dep.target.addDep(this); //先让 watcher 记住dep

   // dep 与 watcher 是一个 多对多的关系 (一个属性可以在多个组件中使用: dep --> 多个watcher)
   // 一个组件由多个属性组成(watcher --> 多个组件)
}
 addSub(watcher) {  // 将记住 dep 的 watcher 也放进 subs, 让 dep 也记住 watcher
   this.subs.push(watcher)
}
 notify() {
   this.subs.forEach(watcher => watcher.update()); //告诉所有用到这个属性的 watcher 需要更新
}
}
Dep.target = null;


export default Dep;

watcher.js

import Dep from "./dep";

let id = 0;

// 每个属性都有一个 dep(属性是被观察者), watcher 就是观察者(属性变化了会通知观察者来更新) --> 观察者模式

class Watcher{
 constructor(vm, fn, options) {
   this.id = id++;
   this.renderWatcher = options; //是一个渲染 watcher
   this.getter = fn; //调用这个函数可以发生 取值操作 (vm._update(vm._render()))
   this.deps = [];  //在 watcher 存放 dep (计算属性、一些清理工作需要用到)
   this.depId = new Set(); //用来判断是否有重复的属性
   this.get();
}
 get() {
   Dep.target = this;  //将当前的渲染 watcher 放到 Dep.target 上
   this.getter(); //去 vm 上取值
   Dep.target = null;  //渲染完毕后,就清空
}
 addDep(dep) { //一个组件 对应着 多个属性 重复的属性不用记录
   let id = dep.id;
   if(!this.depId.has(id)) {
     this.deps.push(dep); //没有这个属性的话,就直接记录在 deps 中
     this.depId.add(id); //记录当前属性 id
     dep.addSub(this);  //watcher 已经记住 dep 而且去重了, 需要让 dep 也记住 watcher
  }
}
 update() { //更新操作: 重新调用一次 get() 方法
   this.get();
}
}

export default Watcher

 

步骤详解:

  • lifecycle.js 模块中的 mountComponent 函数中 new 一个 Watcher 传入更新模块的方法

const updateComponent = () => {
   vm._update(vm._render())
}
 new Watcher(vm, updateComponent,true) //true 用于标识 是一个渲染 watcher
  • 将更新模板的方法 放进 watcher 里面调用 ,并将当前渲染的 watcher 放到 Dep.tartger 上(/observe/watcher.js)

let id = 0;
class Watcher{
 constructor(vm, fn, options) {
   this.id = id++;
   this.renderWatcher = options; //是一个渲染 watcher
   this.getter = fn; //调用这个函数可以发生 取值操作 (vm._update(vm._render()))
   this.deps = [];  //在 watcher 存放 dep (计算属性、一些清理工作需要用到)
   this.depId = new Set(); //用来判断是否有重复的属性
   this.get();
}
 get() {
   Dep.target = this;  //将当前的渲染 watcher 放到 Dep.target 上
   this.getter(); //去 vm 上取值
   Dep.target = null;  //渲染完毕后,就清空
}
}
  • 需要为每个属性增加一个 dep(收集器),用来收集 watcher

let id = 0;
class Dep {
 constructor() {
   this.id = id++;  //属性的 dep 要收集 watcher
   this.subs = [];  // 存放着当前属性对应的 watcher
}
}
Dep.target = null;
  • watcher 执行 this.getter(); 发生取值操作(get方法)的时候。 就让 这个属性的收集器 记住当前的 watcher (/observe/index.js)

let dep = new Dep(); //每个属性都有一个 dep
Object.defineProperty(target, key, {
   get() {  //取值时候,执行 get
     if(Dep.target) { //如果存在 Dep.target 的话,就调用 depend() 收集起来
       dep.depend(); //让这个属性的收集器 记住当前的 watcher
    }
     return value;
  }
}
  • 让这个属性的收集器 记住当前的 watcher 之前, 当前的 watcher 也必须 记住对应的属性,且去掉重复的属性 (/observe/dep.js)

let id = 0;
class Dep {
 constructor() {
   this.id = id++;  //属性的 dep 要收集 watcher
   this.subs = [];  // 存放着当前属性对应的 watcher
}
 depend() {
   Dep.target.addDep(this); //先让 watcher 记住dep

   // dep 与 watcher 是一个 多对多的关系 (一个属性可以在多个组件中使用: dep --> 多个watcher)
   // 一个组件由多个属性组成(watcher --> 多个组件)
}
}
Dep.target = null;
  • 让当前的 watcher 记住对应的属性,且去掉重复的属性 。使用 addDep() 方法,先让当前的 watcher 记住属性,然后再将当前的 watcher 传给 dep , 让 dep 也记住对应的watcher (/observe/watcher)

addDep(dep) { //一个组件 对应着 多个属性  重复的属性不用记录
   let id = dep.id;
   if(!this.depId.has(id)) {
     this.deps.push(dep); //没有这个属性的话,就直接记录在 deps 中
     this.depId.add(id); //记录当前属性 id
     dep.addSub(this);  //watcher 已经记住 dep 而且去重了, 需要让 dep 也记住 watcher
  }
}
  • 使用addSub(watcher),接收已经记住 dep 的 watcher。然后放进 subs, 让 dep 也记住 watcher (/observe/dep)

addSub(watcher) {  // 将记住 dep 的 watcher 也放进 subs, 让 dep 也记住 watcher
   this.subs.push(watcher)
}

更新的时候

  • 在 更新 调用 set() 的时候,会调用 dep.notify(); 通知 dep 要进行更新了 (/observe/index)

dep.notify(); //通知更新
  • 在 dep.js 中遍历 subs 中的 watcher,通知所有用到这个属性的 watcher 需要更新了 (/observe/dep)

notify() {
   this.subs.forEach(watcher => watcher.update()); //告诉所有用到这个属性的 watcher 需要更新了
  • 在 watcher 中新增一个 update 属性进行更新操作

update() { //更新操作: 重新调用一次 get() 方法
   this.get();
}
进行异步更新
  • 异步更新:先将当前的 watcher 暂存起来,之后统一进行更新 (/observer/watcher.js)

update() { //异步更新:先将当前的 watcher 暂存起来,之后统一进行更新
   queueWatcher(this);  
}
  • 将当前需要更新的一次放进一个队列中,然后在同一时间依次的进行更新

let queue = []; //存放 watcher 的数组
let has = {}; //去重
let pending = false; //防抖操作

// 刷新调度队列
function flushSchedulerQueue() {
 let flushQueue = queue.slice(0)

 // 先清空队列
 queue = [];
 has = {};
 pending = false;

 flushQueue.forEach(q => q.run()) //执行的过程中可能还有新的 watcher 进来,会重新放到 queue 中
}


function queueWatcher(watcher) {
 const id = watcher.id;
 if(!has[id]) { //去重
   queue.push(watcher);
   has[id] = true;
   // 不管update 执行多少次,最终只执行一轮刷新操作
   if(!pending) {
     nextTick(flushSchedulerQueue, 0);
     pending = true;
  }
}
}
  • $nextTick的原理就是这样。但是vue2 内部的nextTick中采用的是 优雅降级 的方法

  • 内部先使用 promise(不兼容) 不行的话换 MutationObserver(h5的api) 再不行换 setImmediate(ie专享) 最后还不行换 setTimeout。

let timerFunc;
if(Promise) {
 timerFunc = () => {
   Promise.resolve().then(flushCallbacks)
}
} else if(MutationObserver) {
 let observer = new MutationObserver(flushCallbacks); //这里传入的回调是异步执行的
 let textNode = document.createTextNode(1);
 observer.observe(textNode, {
   characterData:true
});
 timerFunc = () => {
   textNode.textContent = 2
}
} else if(setImmediate) {
 timerFunc = () => {
   setImmediate(flushCallbacks)
}
} else {
 timerFunc = () => {
   setTimeout(flushCallbacks)
}
}

export function nextTick(cb) { //收集所有异步更新的操作
 callbacks.push(cb); //维护nextTick 中的 callback 方法
 if(!waiting) {
   timerFunc(); //看内部使用的是那种方法
   waiting  = true;
}
}
进行数组更新
  • Observe 类中先给每个对象上的下划线 ob 增加收集功能 (/observer/index)

// 给每个对象都增加收集功能
   this.dep = new Dep();
  • get 方法中 让数组与对象本身也实现依赖收集,并判断如果是数组的话,使用 dependArray 方法进行收集

get() {  //取值时候,执行 get
     if(Dep.target) { //如果存在 Dep.target 的话,就调用 depend() 收集起来
       dep.depend(); //让这个属性的收集器 记住当前的 watcher
       if(childOb) {
         childOb.dep.depend();  //让数组与对象本身也实现依赖收集

         if(Array.isArray(value)) { //如果是数组的话,使用 dependArray 的方法进行依赖收集
           dependArray(value);
        }
      }
    }
     return value;
  }
  • dependArray 方法 对数组进行依赖收集

//对数组进行 依赖收集
function dependArray(value) {
 for(let i = 0; i < value.length; i++) {
   let current = value[i];
   current.__ob__ && current.__ob__.dep.depend(); //手动进行依赖收集
   if(Array.isArray(current)) { //数组内可能还是包含数组,所以要对内层的数组进行递归操作
     dependArray(current);
  }
}
}
  • 并且在数组发生变化后,通知对应的 watcher 来实现更新逻辑 (/observe/array.js)

ob.dep.notify(); //数组变化后,通知对应的 watcher 实现更新逻辑
 
网友评论