需要手写源码的可以私信我找我拿哦~~~~
响应式数据变化
数据发生变化后,我们可以监听到这个数据的变化 (每一步后面的括号是表示在那个模块进行的操作)
手写简单的响应式数据的实现(对象属性劫持、深度属性劫持、数组函数劫持)、模板转成 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 模块的全部代码。建议先看一遍整体代码,再看一遍一步一步的解析,然后再看一遍整体代码。这样食用效果更佳哦~
步骤详解:
-
在 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(); }
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
进行异步更新
-
异步更新:先将当前的 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 实现更新逻辑
计算属性
计算属性就是一个 defineproperty。且计算属性底层就是一个带有 dirty 属性的 watcher
-
先会判断有没有存在 computed (static.js)
export function initState(vm) { const opts = vm.$options //获取所有的选项 if (opts.data) { //判断是否存在data initData(vm) } if (opts.computed) { //判断是否存在 computed initComputed(vm) } }
-
在 initComputed 函数中,获取到这个 computed,并存到 vm上。再为每个计算属性 增加一个lazy属性标识
// 对每个计算属性进行操作 function initComputed(vm) { const computed = vm.$options.computed //获取用户自己写的 computed const watchers = vm._computedWatchers = {} //存放每个属性的 watcher,并将计算属性保存到 vm 上。 for (let key in computed) { //拿到每一个计算属性 let userDef = computed[key] let fn = typeof userDef === 'function' ? userDef : userDef.get//查看是不是函数,如果是那么本身就是个 get ,如果不是,那么取里面的 get 方法 // 增加一个 lazy 属性,不要调用直接默认执行 fn。将属性和 watcher 对应起来 watchers[key] = new Watcher(vm, fn, { lazy: true }) defineComputed(vm, key, userDef) } }
-
在 defineComputed 中,通过 Object.defineProperty 拿到每个计算属性对应的属性
//通过实例 拿到每个计算属性对应的属性 function defineComputed(target, key, userDef) { const setter = userDef.set || (() => {}) //set要么有,要么没有 Object.defineProperty(target, key, { get:createComputedGetter(key), set:setter }) }
-
在 createComputedGetter 获取对应属性的 watcher ,然后查看是不是脏值,是就去执行用户传进的 函数
// 检测是否需要执行这个 getter function createComputedGetter(key) { return function () { const watcher = this._computedWatchers[key]; //获取对应属性上的 watcher if(watcher.dirty) { // 如果是脏的话,就去执行用户传入的函数 watcher.evaluate() } return watcher.value; //最后返回的是 watcher 上的值 } }
-
在 watcher 中,获取到计算属性上的 lazy 值,并为每个计算属性都设置一个脏值 (/observe/watcher.js)
this.lazy = options.lazy; this.dirty = this.lazy; //缓存值 this.lazy ? undefined : this.get(); //lazy 为true时,什么都不做,否则就进行依赖收集
-
将 get方法 改为入栈和出栈
get() { pushTarget(this) //将当前的渲染 watcher 放到 Dep.target 上 let value = this.getter.call(this.vm); //去 vm 上取值 popTarget() //渲染完毕后,就清空 return value; }
-
在 dep.js 中维护这个栈 (observe/dep.js)
Dep.target = null; let stack = []; //维护一个栈 export function pushTarget(watcher) { //入栈 stack.push(watcher) Dep.target = watcher; } export function popTarget() { //出栈 stack.pop(); Dep.target = stack[stack.length - 1] }
-
然后开始执行 evaluate ,并将该属性 的 dirty置为 false (observe/watcher.js)
evaluate() { this.value = this.get(); // 获取到用户函数的返回值 并且标识为脏 this.dirty = false; }
-
更新计算属性内部的值,计算属性也要发生变化。所以就需要让 计算属性 watcher的属性,也去收集上一层的watcher
// 检测是否需要执行这个 getter function createComputedGetter(key) { return function () { const watcher = this._computedWatchers[key]; //获取对应属性上的 watcher if(watcher.dirty) { // 如果是脏的话,就去执行用户传入的函数 watcher.evaluate() } if(Dep.target) { //计算属性出栈后,还需要渲染 watcher,需要让计算属性 watcher 里面的属性,也去收集上一层的 watcher watcher.depend(); } return watcher.value; //最后返回的是 watcher 上的值 } }
-
在 watcher 中增加 depend 方法,然后让 每个 watcher 都调用 dep中的 depend方法。进行收集渲染watcher (/observe/watcher)
depend() { let i = this.deps.length; while(i--) { //调用 dep中的 depend 方法 this.deps[i].depend(); //让计算属性 watcher 也收集渲染 watcher } }
监听属性
-
先判断是否存在 watch (state.js)
export function initState(vm) { const opts = vm.$options //获取所有的选项 if (opts.data) { //判断是否存在data initData(vm) } if (opts.computed) { //判断是否存在 computed initComputed(vm) } if (opts.watch) { // 判断是否存在 watch initWatch(vm) } }
-
判断 watch 是 字符串还是数组 还是 函数 (state.js)
function initWatch(vm) { let watch = vm.$options.watch for(let key in watch) { const handler = watch[key] //看一下是字符串、数组、函数 if(Array.isArray(handler)) { for(let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]); } } else { createWatcher(vm, key, handler); } } } function createWatcher(vm, key, handler) { //判断是字符串还是函数 if(typeof handler === 'string') { handler = vm[handler]; } return vm.$watch(key, handler) }
-
在 index.js 中 挂载一个 $watch 方法。watch无论是字符串、数组、函数形式,都走这个方法
// watch 底层调用的都是这个方法 Vue.prototype.$watch = function (exprOrFn, cb) { // name 值变化了,直接执行cb 回调函数即可 new Watcher(this. exprOrFn, {user:true}, cb) }
-
在 watcher.js 中,在调用取值操作前,将字符串形式也转成函数形式。
if(typeof exprOrFn === 'string') { //如果是字符串的话,也要将它转化为函数 this.getter = function() { return vm[exprOrFn]; } } else { this.getter = exprOrFn; //调用这个函数可以发生 取值操作 (vm._update(vm._render())) }
-
然后在进行更新的时候,调用回调函数,传入新旧值进行判断更新
run() { //进行更新 let oldValue = this.value; let newValue = this.get(); if(this.user) { this.cb.call(this.vm, newValue, oldValue) } }