回顾:
- 将数据先处理成响应式
inintState
(针对对象来说主要是增加defineProperty 针对数组就是重写方法) - 模板编译:将模板通过正则匹配转换成AST语法树,然后将AST语法树生成
render
方法 - 调用render函数,将HTML模板转换成JS代码,并在实例中进行取值操作,产生对应的虚拟DOM
render(){ _c('tag', attrs, children)}
出发get方法 - 将虚拟DOM渲染成真实DOM
这节主要内容
- 观察者模式实现依赖收集
- 异步更新策略
- mixin的实现原理
依赖收集
- 给模板中的属性增加一个收集器
dep
,- 页面渲染的时候将渲染逻辑封装到watcher中,vm._update(vm._render())
- 让页面记住这个watcher即可,稍后属性变化了可以找到对应的dep中存放的watcher进行重新渲染
组件化的好处:复用、方便维护、局部更新
在属性劫持的方法中: 当属性取值时执行get方法,判断当前属性的Watcher是否存在,存在则调用dep.depend()
为当前watcher添加属性dep(不重复),更新值则调用set方法执行dep.notify()
对数据进行更新(对dep中的每个watcher都遍历更新调用render函数
)
export function defineReactive(target, key, value){//闭包 属性劫持
// debugger
observe(value) //对多层对象递归进行属性劫持
let dep = new Dep();//每一个属性都有一个Dep
Object.defineProperty(target, key, {
get(){//取值的时候会执行get
if(Dep.target){
dep.depend(); //让这个属性的收集器记住当前的watcher,只有需要渲染取值的属性才会去收集
}
return value
},
set(newV){
if(newV === value) return
observe(newV)
value = newV //Q_lys:这里有一点不是很明白,defineReactive中的参数value应该只在defineReactive的函数内部有效,为什么这里直接更改value会反向更改data中的值
// console.log('set data:', target)
dep.notity()//通知更新
}
})
}
dep.js:每个属性都有一个Dep,属性获取值时会进行判断当前watcher(视图)中是否已存在该dep,没有则添加,并且也为该dep添加watcher
import watcher from "./watcher";
let id = 0;
class Dep{
constructor(){
this.id = id++;//属性的deo要收集watcher
this.subs = [];//这里存放着当前属性对应的watcher有哪些
}
depend(){
// this.subs.push(Dep.target);
//不希望放置重复watcher
Dep.target.addDep(this)
//dep和watcher是一个多对多的关系 (一个属性可以在多个组件中使用dep->watcher)
//一个组件可以有多个属性
}
addSub(watcher){
this.subs.push(watcher)
}
notify(){
this.subs.forEach(watcher=>watcher.update());//告诉watcher要更新了
}
}
Dep.target = null
export default Dep
watcher.js:观察者模式:每个属性有个dep(属性就是被观察者),watcher就是观察者(属性变化了会通知观察者来更新)
- 当我们创建渲染watcher的时候我们会把当前的渲染watcher放到Dep.target上
- 调用_render() 会取值 走到 get上
import Dep from "./dep";
// 观察者模式:每个属性有个dep(属性就是被观察者),watcher就是观察者(属性变化了会通知观察者来更新)
// 1.当我们创建渲染watcher的时候我们会把当前的渲染watcher放到Dep.target上
// 2.调用_render() 会取值 走到 get上
let id = 0;
class watcher{
//options为true则标识是渲染过程
constructor(vm, fn, options){//不同组件有不同watcher 目前只有一个 渲染根实例
this.id = id++;
this.renderWatcher = options;//是一个渲染watcher
this.getter = fn;//getter意味着调用这个函数可以发生取值操作
this.deps = [];//让watch记住deps 后续实现计算属性和一些清理工作需要用到
this.depsId = new Set()
this.get()
}
addDep(dep){//一个组件有多个属性,重复属性不用记录
let id = dep.id;
if(!this.depsId.has(id)){
this.deps.push(dep);
this.depsId.add(id);
dep.addSub(this);//watcher已经记住dep且去重了,此时让dep也记住了watcher
}
}
get(){
Dep.target = this;//静态属性只有一份
this.getter();//会去vm上取值
Dep.target = null;//渲染完毕就清空
}
update(){
this.get();//重新渲染
}
}
//需要给每个属性增加一个dep 目的就是收集watcher
// 一个视图(zujian )对应一个watcher n个属性对应一个视图 n个dep对应一个watcher
//1个属性对应多个视图(组件中可能出现同名属性) 1个dep对应多个watcher
//多对多关系
export default watcher
异步更新
上面的操作,如果每次更新num,都会调用render函数,现在考虑只有同步代码都执行完之后,才会开始刷新渲染调用render函数。
vm.name = 'jw',
vm.age = 30,
vm.name = 'lys',
vm.name = 'hzt',
vm.name = 'zyl'
一共调用五次update
异步更新也是一种优化策略,只有当同步代码vm.name = 'zyl'
执行完才会开始渲染
watcher.js中改变: 当数据发生改变时在所劫持的set方法中调用dep.notify()
通知所依赖的watcher调用update方法,现在在update中将会把当前的watcher暂存到队列中,队列中会进行去重操作,并且只有第一次放入队列时才会开启定时器,只有在主线程执行完毕后才会回调flushSchedulerQueue
函数将队列中的watcher拿出来依次执行get()
方法刷新页面
class watcher{
....
update(){
// this.get();//重新渲染
queueWatcher(this);//把当前的watcher暂存起来
}
run(){
console.log("run")
this.get()
}
}
let queue = []
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;//不管uodate执行多少次,最终只执行一轮刷新操作
if(!pending){
setTimeout(flushSchedulerQueue, 0);//定时器只会在主执行栈执行完后才会调用回调函数
pending = true;
}
}
}
nextTick在Vue源码内部没有直接使用某个api而是采用优雅降级的方式
- 内部先采用promise(ie不兼容)
- MutationObserver(h5的api)
- ie专享的setImmediate
- setTimeout
let callbacks = [];
let waiting = false;
function flushCallbacks(){
let cbs = callbacks.slice(0);
waiting = true;
callbacks = [];
cbs.forEach(cb=>cb());//按照顺序依次执行
}
export function nextTick(cb){
callbacks.push(cb);//维护nextTick中的callback方法
if(!waiting){//等主程序中代码执行完毕,最后一起刷新执行回调队列中的回调函数
timerFunc();
waiting = true;
}
}
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,{//监控文本变化,当timerFunc调用时将文本内容改变后,则执行flushCallbacks
characterData: true
});
timerFunc = ()=>{
textNode.textContent = 2
}
}else if(setImmediate){
timerFunc = ()=>{
setImmediate(flushCallbacks);
}
}else{
timerFunc = () => {
setTimeout(flushCallbacks)
}
}
mixin实现原理
将用户选项和全局的options进行合并
Vue.options = {}
Vue.mixin = function (mixin) {
// debugger
//将用户的选项和全局的options进行合并
this.options = mergeOptions(this.options, mixin)
return this;
}
const strats = {}
const LIFECYCLE = ['beforeCreate', 'created'];
LIFECYCLE.forEach(hook => {
strats[hook] = function (p, c) {
if (c) {
if (p) {
return p.concat(c);
} else {
return [c];//儿子有父亲没有,第一次拼接,则将儿子包装成数组
}
} else {
return p;
}
}
})
export function mergeOptions(parent, child) {
const options = {};
for (let key in parent) {
mergeField(key);
}
for (let key in child) {
//hsaOwnProperty
if (!parent.hasOwnProperty(key)) {
mergeField(key)
}
}
function mergeField(key) {
if (strats[key]) {
options[key] = strats[key](parent[key], child[key])
} else {
options[key] = child[key] || parent[key] //取值时如果不在策略中优先采用儿子,再采用父亲
}
}
return options
}