zjn225 / zjn225.github.io Goto Github PK
View Code? Open in Web Editor NEW重写的个人博客
Home Page: https://zjn225.github.io
重写的个人博客
Home Page: https://zjn225.github.io
关键点:数据劫持、发布者订阅者模式(依赖收集) + Observer、Watcher、Dep
1、有一个监听器Observer,给data及data下的数组对象循环调用Object.defineProperty方法来设置getter和setter,以此来劫持监听data的赋值和取值。
2、有多个订阅者Watcher,在初始化的时候会将自己装进订阅器Dep中(强制get,这样Watcher就能和Dep关联起来)
3、关于监听器Observer的set和get:
① 当读取所需对象的值时,会触发getter函数进行依赖收集,目的是将相关Watcher对象存放到的Dep的subs中,形成的关系如下。
② 在修改对象的值的时候,会触发相应的setter,setter通知之前依赖收集得到的订阅器Dep中的每一个Watcher,这些watcher都依赖着这个值,然后会告诉他们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图,当然这中间还有一个 patch和re-render 的过程 (setter-->watcher-update)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Two-way-data-binding</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text"> {{ text }}
</div>
<script>
// 双向绑定第一步:view--->model
// 监听器Observer,对vm进行数据劫持
function observe(obj, vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm, key, obj[key]);
})
}
function defineReactive(obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function () {
// 进行依赖收集,会将相关Watcher对象存放到Dep中
// update函数里的get方法而来,执行完update方法之后target才为null
if (Dep.target) dep.addSub(Dep.target);
return val
},
set: function (newVal) {
if (newVal === val) return
val = newVal;
// 作为发布者发出通知,通知Dep的每一个Watcher
dep.notify();
}
});
}
// 订阅者Watcher
function Watcher(vm, node, name, nodeType) {
Dep.target = this; //指向自己
this.name = name;
this.node = node;
this.vm = vm;
this.nodeType = nodeType;
// 执行监听器里的get函数,将自己装进订阅器Dep中,这样就能把Watcher和Dep关联起来
this.update();
Dep.target = null;// 当实例化之后,释放自己
}
Watcher.prototype = {
update: function () {
this.get();
if (this.nodeType == 'text') {
this.node.nodeValue = this.value;
}
if (this.nodeType == 'input') {
this.node.value = this.value;
}
},
// 获取 data 中的属性值
get: function () {
this.value = this.vm[this.name]; // 触发相应属性的 get
}
}
// 订阅器Dep
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub: function (sub) {
this.subs.push(sub);
},
notify: function () {
this.subs.forEach(function (sub) {
sub.update();
});
}
}
// 双向绑定第二步:model-->view
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment();
var child;
// appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
// 所以,第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
while (child = node.firstChild) {
compile(child, vm);//child分别是空白元素、input、{{text}}
flag.appendChild(child); // 将子节点劫持到文档片段中
}
return flag
}
function compile(node, vm) {
// 节点类型为元素
if (node.nodeType === 1) {
// 获取元素属性后循环解析
var attr = node.attributes;
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue; // 获取 v-model 绑定的属性名,text
node.addEventListener('input', function (e) {
// 给相应的 data 属性赋值,进而触发该属性的 set 方法(数据劫持里)
vm[name] = e.target.value;
});
node.value = vm[name]; // 将 data 的值赋给该 node
node.removeAttribute('v-model'); //把节点的v-model属性删除
}
};
new Watcher(vm, node, name, 'input');
}
// 节点类型为 text
if (node.nodeType === 3) {
var reg = /\{\{(.*)\}\}/;
if (reg.test(node.nodeValue)) { // {{text}}
var name = RegExp.$1; // 获取匹配到的字符串text
name = name.trim();
new Watcher(vm, node, name, 'text');
}
}
}
function Vue(options) {
debugger;
this.data = options.data;
//view--->model
observe(this.data, this);//this就是vm
// model-view
var id = options.el;
var dom = nodeToFragment(document.getElementById(id), this);//this就是vm
// 编译完成后,将 dom 返回到 app 中
document.getElementById(id).appendChild(dom);
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world你好呀世界'
}
})
</script>
</body>
</html>
Vue 组件挂载时添加响应式的过程。在组件挂载时,会先对所有需要的属性调用 Object.defineProperty(),然后实例化 Watcher,传入组件更新的回调。在实例化过程中,会对模板中的属性进行求值,触发依赖收集。
(一)先是编译流程,model->view
new Vue,执行Vue构造函数,里面是双向绑定的入口函数observe和nodeToFragment
先执行observer,劫持了get和set方法,注意此时里面的方法还是没执行的
执行nodeToFragment,循环获取firstChild后依次编译
编译过程,如果是元素类型的节点,刚开始是input元素
a、获取元素attributes后循环解析,找出nodeName为v-model的变量
b、然后刚进入页面时没有input事件的,所以将v-model绑定变量的初始值赋给node.value
c、执行Watcher方法,初始化的时候执行了update,update里强制执行了vm的get,这样Wacher把自己加入了Dep中,
d、然后update方法里判断出是input,把input的node.value赋值为刚刚v-model的node.value,这样输入框就默认获取到了Vue的data里的参数了
编译过程,其次是{{text}}的解析,和解析input一样的步骤,最终是把{{text}}字样也改为刚刚的value值
(二)手动改变输入框的值,view->model
1.执行数据劫持里的set方法,执行Dep的notify方法,遍历每个sub执行update方法。
Function.prototype.myCall = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
context = context || window
const mySymbol = Symbol() // 利用Symbol而不是直接在context添加属性,保证了不会覆盖已有属性
context[mySymbol] = this // 这里的this,是xx.Mycall的xx函数
const args = [...arguments].slice(1) // 获取第二个参数开始的参数,也就是除了上下文外的参数
const result = context[mySymbol](...args) // call可以传入多个参数作为调用函数的参数
delete context[mySymbol]
return result
}
测试
window.value = "outer"
let foo = {
value: "inner"
}
let bar = function () {
console.log("this", this)
console.log("value", this.value)
console.log("------------")
}
bar.myCall(foo); // value为inner
context[mySymbol] = this,这一步非常关键,用一个变量直接代替 context[mySymbol]是不行的,因为xx.call(yy)是要用yy来执行xx的函数,context里才有yy的上下文,变量是没有的,否则会导致上下文的丢失,直接为window了
let that = context[mySymbol];
that(...args) // 这样子虽然that和context[mySymbol]指针是一样的,但是此时的上下文是指向context[mySymbol]整体的,而不是context
apply接收的第二个参数是数组形式,这是唯一的不同点
Function.prototype.myApply = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
let result;
context = context || window
const mySymbol = Symbol()
context[mySymbol] = this
if (arguments[1]) {
result = context[mySymbol](...arguments[1])
} else {
result = context[mySymbol]()
}
delete context[mySymbol]
return result
}
bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数
this.value = 2
var foo = {
value: 1
}
var bar = function() {
console.log(this.value)
}
var result = bar.bind(foo)
bar() // 2
result() // 1,即this === foo
this.value = 2
var foo = {
value: 1
};
var bar = function(name, age, school) {
console.log(name) // 'An'
console.log(age) // 22
console.log(school) // '广东财经大学'
}
var result = bar.bind(foo, 'An') //预置了部分参数'An'
result(22, '广东财经大学') //返回的函数能继续传入参数
(前提里面得有this.xxx才能赋值)
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind(foo, 'daisy');
// 1
// daisy
var obj = new bindFoo('18');
obj.friend === 'kevin' // 测试new后原型链是否继承父类
// obj == {habit: "shopping"} obj.friend = 'kevin'
// 实例化的时候也执行了bar函数,此时this是指向bar本身的,所以this.value == undefined,name = "daisy",age=18
// 初始版本,未考虑到原型链,在new一个bind生成的函数的时候,没有继承原函数的原型
Function.prototype.bind2 = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
const _this = this
const args = [...arguments].slice(1)
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
return _this.apply(this, args.concat(...arguments))
}
return _this.apply(context, args.concat(...arguments))
}
}
// 完全版本
Function.prototype.bind2 = function (oThis) {
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
that = this, //保存this值,指的调用bind的函数也就是bar
fNOP = function () { }, //原型式继承的第一步
fBound = function () { //要返回的函数等于后面的foo,再次调用时才执行这里面的代码
//直接bind的话this=window,new了this=fBound,而fBound.__proto__ = FNOP
return that.apply(this instanceof fNOP ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)));
};
// 以下两步和上面初始化FNOP相当于原型式继承,等于fbound.prototype = Object.create(this.prototype);
// 目的是为了在实例化的时候,能让 fBound 构造的实例能够继承绑定函数bar的原型中的值
fNOP.prototype = that.prototype;
fBound.prototype = new fNOP(); // fBound.__proto__ = FNOP
return fBound;
};
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind2(foo, 'daisy');
// 1
// daisy
//当不实例化时,this instanceof FNOP = false,由于是直接执行foo(),相当于window.foo(),所以内部this是window,that是bar
//当实例化后bind出来的新函数后,this是指向本身的,this instanceof FNOP = true,内部this是fBound,而fBound.__proto__ = FNOP,that是bar
var obj = new bindFoo('18');
obj.friend === 'kevin' // 测试new后原型链是否继承父类
<template>
<div>
<div ref="test">{{test}}</div>
<button @click="handleClick">按钮</button>
</div>
</template>
export default {
data () {
return {
test: '1'
};
},
methods () {
handleClick () {
this.test = '2';
console.log(this.$refs.test.innerText);//打印“1”
}
}
}
以上代码中,当点击这个按钮触发handleClick修改dom来设置值的时候,如果是同步更新DOM的话,输出的1,但是输出的是2,就证明了Vue DOM的更新不是同步的。
Vue官方文档如是说:
可能你还没有注意到,Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。
例如,当你设置vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。多数情况我们不需要关心这个过程,但是如果你想在 DOM 状态更新后做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们确实要这么做。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。
Vue的数据驱动视图更新是异步的,即修改数据时,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。
本质上都是调用了同一个方法,绑定时候的源码如下:
export function renderMixin (Vue: Class<Component>) {
// 省略...
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
// 省略...
}
然后$nextTick 方法是在renderMixin 函数中挂载到Vue原型上的,$nextTick只是对nextTick函数的简单包装。但是会传入一个this,所以在使用实例方法$nextTick时,可以不指定上下文
getData(res).then(()=>{
this.xxx = res.data
this.$nextTick(() => {
// 这里我们可以获取变化后的 DOM
})
})
去年Vue 2.5开始就单独抽出一个文件了,然后这个是2019.1.24最新更新的代码,2.6.7
/* @flow */ // 静态类型检测
import { noop } from 'shared/util' // 空函数
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false // 是否使用了microTask
const callbacks = [] // 存储所有需要执行的回调函数
let pending = false // 用来标志是否正在执行回调函数
// 用来执行callbacks里存储的所有回调函数。
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0 // 保存回调函数后清空
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc // 宿主环境不同,可能会是以下四种的其中一种
// 检查宿主环境是否支持promise,如果支持,则利用promise来触发执行回调函数;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop) // IOS兼容,虽然能触发微任务,但是这个微任务不会更新,需要浏览器做一些事情比如注册一个macrotask,即使这个macrotask什么都不做
}
isUsingMicroTask = true
// 如果支持MutationObserver(HTML5中的新API,是个用来监视DOM变动的接口),则实例化一个观察者对象,观察文本节点发生变化时,触发执行所有回调函数。
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks) // 得到实例,里面的回调函数会在实例监听的dom发生改动时触发
const textNode = document.createTextNode(String(counter)) // 创建一个观测对象
observer.observe(textNode, {
characterData: true //说明监听的dom文本内容有修改
})
timerFunc = () => {
// 修改counter,让实例监听到dom变化,进而执行回调
// 归根到底就是nextTick想借用这个异步API来执行回调,也就是利用了MutationObserver的回调会被当作microTask来执行
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
// 检测是否支持setImmediate,此时isUsingMicroTask是false,表示此时所有的回调都注册为宏任务了
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
// 都不支持的话就调用setTimeout,此时isUsingMicroTask是false,表示此时所有的回调都注册为宏任务了
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// 之所以是以上的顺序,是因为执行优先级、以及性能
/*
推送到队列中下一个tick时执行
cb 回调函数
ctx 上下文
*/
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 传入的cb会被push进callbacks中存放起来
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 先判断是否正在执行回调,如果不在执行,就将pengding设为true,然后执行timerFunc
if (!pending) {
pending = true
timerFunc()
}
// 这是当 nextTick 不传 cb 参数的时候,提供一个 Promise 化的调用,比如:nextTick().then(() => {})
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
官方文档做出过如下解释
在Vue 2.5之前的版本中,nextTick几乎都是基于microTask实现的(大概是大部分宿主环境都是比较高级了,所以默认都是Promise了?)但是由于microTask的执行优先级非常高,在某些场景之下它甚至要比事件冒泡还要快,就会导致一些诡异的问题;但是如果全部都改成macroTask,对一些有重绘和动画的场景也会有性能的影响。
所以最终在2.5的时候nextTick采取的策略是默认走microTask,对于一些DOM的交互事件,如v-on绑定的事件回调处理函数的处理,会强制走macroTask。
具体做法是,在Vue绑定DOM事件时,默认会给回调函数调用withMacroTask方法做一层包装,从而保证整个回调函数的执行过程中,遇到数据改变导致的DOM更新相关的任务都会被推到macroTask。对于 macro task 的执行,Vue.js 优先检测是否支持原生 setImmediate,不支持的话就会降级为 setTimeout 0。代码如下,这段代码也是在同一个文件的。
目的:共享组件之间的状态和逻辑,提高可复用性
import React, { Component } from 'react'
export default (WrappedComponent, name) => {
class NewComponent extends Component {
constructor () {
super()
this.state = { data: null }
}
componentWillMount () {
let data = localStorage.getItem(name)
this.setState({ data })
}
render () {
return <WrappedComponent data={this.state.data} />
}
}
return NewComponent
}
初始化工作:现在 NewComponent 会根据第二个参数 name 在挂载阶段从 LocalStorage 加载数据,并且 setState 到自己的 state.data 中,而渲染的时候将 state.data 通过 props.data 传给 WrappedComponent(即子组件)。
假设上面的代码是在 src/wrapWithLoadData.js 文件中的,我们可以在别的地方这么用它:
import wrapWithLoadData from './wrapWithLoadData'
class InputWithUserName extends Component {
render () {
return <input value={this.props.data} />
}
}
InputWithUserName = wrapWithLoadData(InputWithUserName, 'username')
export default InputWithUserName
这里的this.props来自HOC即wrapWithLoadData,父子组件的关系!子组件wrapWithLoadData 接收了父组件的data参数
InputWithUserName组件作为参数传入wrapWithLoadData,渲染的出来的就是InputWithUserName里的render部分,即<input xxx,HOC的render部分相当于父组件一样,起到传递参数的作用
挂载的时候从 LocalStorage 里面加载 username 字段作为 的 value 值
一种在 React 组件之间使用一个值为函数的 prop 共享代码技术,render prop 是一个用于告知组件需要渲染什么内容的函数类型的prop。(由父组件告知)
1、可变数据源(还不知道自己要渲染什么东西)
class List extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
list: props.getList()
};
}
handleChange() {
// 当数据源更新时,更新组件状态
this.setState({
list: props.getList()
});
}
render() {
return (
<div>
{this.props.render(this.state)} // 调用父组件的render方法
</div>
);
}
}
这个render就是我们前面说的“一个值为函数的属性”,叫啥名儿都行,叫render是凑巧了,子组件只负责把“可变数据源”即this.state作为参数给render函数,至于渲染的内容是什么?那就不是子组件能控制了,这个render是调用父组件的render方法
2、如何调用这货
class Page extends React.Component {
//...
render() {
return (
<List getList={DataSource.getItems}
render={list=>(
<div>{list.map(item=>(<Item item={item} key={item.id} />))} </div>
)} />
);
}
}
List组件的this.props.render调用的也就是这个 list=>(<div>{list.map(item=>())} </div>),这个list参数的来源即List组件里的this.state
不编写 class 的情况下使用 state 以及其他的 React 特性(之前的函数式组件无法拥有自己的状态),通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。
1、自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
import React, { useState, useEffect } from 'react';
function useData(getData) {
// useState 会返回一对值:当前状态和一个让你更新它的函数
const [list, setList] = useState([]);
useEffect(() => {
function handleChange() {
setList(getData());
}
DataSource.addChangeListener(this.handleChange);
return () => {
DataSource.removeChangeListener(this.handleChange);
};
});
}
return list;
2、使用Hook
function ItemList(props) {
const list = useData(DataSource.getItems);
return (
<div>
{list.map(item=>(<Item item={item} key={item.id}/>))}
</div>
)
}
1、全局执行上下文
有且只有一个window
2、函数执行上下文
每次调用函数时创建
3、Eval函数执行上下文
运行在eval函数中的代码,不建议使用
1、确定this的值
2、LexicalEnvironment(词法环境)组件被创建
3、VariableEnvironment(变量环境) 组件被创建
变量环境类似词法环境,属性和词法环境一致,在ES6中 的话,区别在于词法环境存储函数声明和变量(let const),而变量环境仅用于存储变量(var)
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
对应的执行上下文的创建阶段
// 全局执行上下文
GlobalExectionContext = {
ThisBinding: <Global Object>, // 1.确定this
LexicalEnvironment: { // 2.词法环境
EnvironmentRecord: { // 2.1环境记录
Type: "Object",
// 标识符绑定在这里
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null> // 2.2 对外部环境的引用
},
VariableEnvironment: { // 3.变量环境
EnvironmentRecord: { // 3.1环境记录
Type: "Object",
// 标识符绑定在这里
c: undefined,
}
outer: <null> // 3.2对外部环境的引用
}
}
// 函数上下文,类似上
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定在这里
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定在这里
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
执行栈,也叫调用栈,用于存储在代码执行期间创建的所以执行上下文,LIFO(后进先出)
运行JS代码时,会创建一个全局执行上下文,然后push到执行栈中,以后每次函数调用,引擎都会为这个函数创建一个函数执行上下文到当前执行栈的栈顶
当栈顶函数执行完后,其对应的函数执行上下文会从执行栈中pop出,上下文的控制权将转移到下一个执行上下文
在函数上下文中,用活动对象AO表示变量对象VO,变量对象不能被访问,当进入到一个执行上下文后,这个VO才会被激活,成为AO,这时各种属性函数才能被访问
保存在栈内存中(除Object外的6种)
/**
* Promise 实现 遵循promise/A+规范
* Promise/A+规范译文:
* https://malcolmyu.github.io/2015/06/12/Promises-A-Plus/#note-4
*/
// promise 三个状态
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
function Promise(excutor) {
let that = this; // 缓存当前promise实例对象
that.status = PENDING; // 初始状态
that.value = undefined; // fulfilled状态时 返回的信息
that.reason = undefined; // rejected状态时 拒绝的原因
that.onFulfilledCallbacks = []; // 存储fulfilled状态对应的onFulfilled函数
that.onRejectedCallbacks = []; // 存储rejected状态对应的onRejected函数
// value为成功态时接收的终值
function resolve(value) {
// 如果value是Promise,则再次执行then
if (value instanceof Promise) {
return value.then(resolve, reject);
}
// 规范规定要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
setTimeout(() => {
// 调用resolve 回调对应onFulfilled函数
if (that.status === PENDING) {
// 只能由pending状态 => fulfilled状态 (避免调用多次resolve reject)
that.status = FULFILLED;
that.value = value;
that.onFulfilledCallbacks.forEach(cb => cb(that.value));
}
});
}
// reason失败态时接收的拒因
function reject(reason) {
setTimeout(() => {
// 调用reject 回调对应onRejected函数
if (that.status === PENDING) {
// 只能由pending状态 => rejected状态 (避免调用多次resolve reject)
that.status = REJECTED;
that.reason = reason;
that.onRejectedCallbacks.forEach(cb => cb(that.reason));
}
});
}
// 捕获在excutor执行器中抛出的异常,栗子如下
// new Promise((resolve, reject) => {
// throw new Error('error in excutor')
// })
try {
excutor(resolve, reject);
} catch (e) {
reject(e);
}
}
/**
* resolve中的值几种情况:
* 1.普通值
* 2.promise对象
* 3.thenable对象/函数,有then方法的函数或对象
* 2和3统一处理
*/
/**
* 用来处理then方法返回结果包装成promise 方便链式调用
* 对resolve 进行改造增强 针对resolve中不同值情况 进行处理
* @param {promise} promise2 promise1.then方法返回的新的promise对象
* @param {[type]} x promise1中onFulfilled或onRejected的返回值
* @param {[type]} resolve promise2的resolve方法,传进来方便更改promise最后的状态
* @param {[type]} reject promise2的reject方法,传进来方便更改promise最后的状态
*/
function resolvePromise(promise2, x, resolve, reject) {
// 如果从onFulfilled中返回的x 就是promise2本身 就会导致循环引用报错,自己等待自己,一辈子等不完
if (promise2 === x) {
return reject(new TypeError('循环引用'));
}
// 用来记录promise2的状态改变,一旦发生改变了 就不允许 再改成其他状态 成功和失败只能只能调用一个 限制既调resolve,也调reject
// 主要是调用then方法的时候用的,和递归没什么关系
let called = false;
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
// 如果x是一个对象或者函数 那么他就有可能是promise 需要注意 null typeof也是 object 所以需要排除掉
// 先获得x中的then 如果这一步发生异常了,那么就直接把异常原因reject掉
try {
let then = x.then;
if (typeof then === 'function') {
// 如果then是个函数 那么就调用then 并且把成功回调和失败回调传进去;如果then是函数,就默认当作是promise了
// 如果此时x是一个promise 并且最终状态时成功,那么就会执行成功的回调,如果失败就会执行失败的回调如果失败了,就把失败的原因reject出去,作为promise2的失败原因,直到成功了就取成功的value
// 以x,也就是Promise作为上下文,value为成功的值,开始执行then方法,这个value有可能仍然是promise,所以需要递归调用resolvePromise这个方法 直达返回值不是一个promise
// 至于为什么用call,因为then.xxx时,this是window而不是Promise本身了
then.call(x, value => {
if (called) return;
called = true;
resolvePromise(promise2, value, resolve, reject)
}, error => {
if (called) return
called = true;
reject(error)
})
} else {
resolve(x)
}
} catch (error) {
if (called) return
called = true;
reject(error)
}
} else {
// 如果是一个普通值 那么就直接把x作为promise2的成功value resolve掉
resolve(x)
}
}
/**
* [注册fulfilled状态/rejected状态对应的回调函数]
* @param {function} onFulfilled fulfilled状态时 执行的函数
* @param {function} onRejected rejected状态时 执行的函数
* @return {function} newPromsie 返回一个新的promise对象
*/
Promise.prototype.then = function (onFulfilled, onRejected) {
const that = this;
let newPromise;
// 处理参数默认值 保证参数后续能够继续执行(处理值穿透)
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
onRejected = typeof onRejected === "function" ? onRejected : reason => { throw reason; };
// then里面的FULFILLED/REJECTED状态时 为什么要加setTimeout ?
// 原因:
// 其一 2.2.4规范 要确保 onFulfilled 和 onRejected 方法异步执行(且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行) 所以要在resolve里加上setTimeout
// 其二 2.2.6规范 对于一个promise,它的then方法可以调用多次.(当在其他程序中多次调用同一个promise的then时 由于之前状态已经为FULFILLED/REJECTED状态,则会走的下面逻辑),所以要确保为FULFILLED/REJECTED状态后 也要异步执行onFulfilled/onRejected
// 其二 2.2.6规范 也是resolve函数里加setTimeout的原因
// 总之都是 让then方法异步执行 也就是确保onFulfilled/onRejected异步执行
// 如下面这种情景 多次调用p1.then
// p1.then((value) => { // 此时p1.status 由pending状态 => fulfilled状态
// console.log(value); // resolve
// // console.log(p1.status); // fulfilled
// p1.then(value => { // 再次p1.then 这时已经为fulfilled状态 走的是fulfilled状态判断里的逻辑 所以我们也要确保判断里面onFuilled异步执行
// console.log(value); // 'resolve'
// });
// console.log('当前执行栈中同步代码');
// })
// console.log('全局执行栈中同步代码');
//
if (that.status === FULFILLED) { // 成功态
return newPromise = new Promise((resolve, reject) => {
setTimeout(() => {
try {
let x = onFulfilled(that.value);
resolvePromise(newPromise, x, resolve, reject); // 新的promise resolve 上一个onFulfilled的返回值
} catch (e) {
reject(e);
}
});
})
}
if (that.status === REJECTED) { // 失败态
return newPromise = new Promise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(that.reason);
resolvePromise(newPromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
if (that.status === PENDING) { // 等待态
// 当异步调用resolve/rejected时 将onFulfilled/onRejected收集暂存到集合中
return newPromise = new Promise((resolve, reject) => {
that.onFulfilledCallbacks.push((value) => {
try {
let x = onFulfilled(value);
resolvePromise(newPromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
that.onRejectedCallbacks.push((reason) => {
try {
let x = onRejected(reason);
resolvePromise(newPromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
};
/**
* Promise.all Promise进行并行处理
* 参数: promise对象组成的数组作为参数
* 返回值: 返回一个Promise实例
* 当这个数组里的所有promise对象全部变为resolve状态的时候,才会resolve。
*/
Promise.all = function (promises) {
return new Promise((resolve, reject) => {
let done = gen(promises.length, resolve);
promises.forEach((promise, index) => {
promise.then((value) => {
done(index, value)
}, reject)
})
})
}
function gen(length, resolve) {
let count = 0;
let values = [];
return function (i, value) {
values[i] = value;
if (++count === length) {
console.log(values);
resolve(values);
}
}
}
/**
* Promise.race
* 参数: 接收 promise对象组成的数组作为参数
* 返回值: 返回一个Promise实例
* 只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理(取决于哪一个更快)
*/
Promise.race = function (promises) {
return new Promise((resolve, reject) => {
promises.forEach((promise, index) => {
promise.then(resolve, reject);
});
});
}
// 用于promise方法链时 捕获前面onFulfilled/onRejected抛出的异常
Promise.prototype.catch = function (onRejected) {
return this.then(null, onRejected);
}
Promise.resolve = function (value) {
return new Promise(resolve => {
resolve(value);
});
}
Promise.reject = function (reason) {
return new Promise((resolve, reject) => {
reject(reason);
});
}
/**
* 基于Promise实现Deferred的
* Deferred和Promise的关系
* - Deferred 拥有 Promise
* - Deferred 具备对 Promise的状态进行操作的特权方法(resolve reject)
*
*参考jQuery.Deferred
*url: http://api.jquery.com/category/deferred-object/
*/
Promise.deferred = function () { // 延迟对象
let defer = {};
defer.promise = new Promise((resolve, reject) => {
defer.resolve = resolve;
defer.reject = reject;
});
return defer;
}
/**
* Promise/A+规范测试
* npm i -g promises-aplus-tests
* promises-aplus-tests Promise.js
*/
try {
module.exports = Promise
} catch (e) {
}
npm i -g promises-aplus-tests
promises-aplus-tests index.js
// 定义三种状态、resolve和reject方法、then方法(onFulfilled、onRejected)
function myPromise(constructor) {
this.status = "pending" //定义初始状态
this.value = undefined;//存储成功后的值
this.reason = undefined;//存储失败的原因
let resolve = value => {
if (this.status === "pending") {
this.status = "resolved";
this.value = value;
}
}
let reject = reason => {
if (this.status === "pending") {
this.status = "rejected";
this.reason = reason;
}
}
//捕获构造异常
try {
constructor(resolve, reject);
} catch (e) {
reject(e);
}
}
myPromise.prototype.then = function (onFulfilled, onRejected) {
// 状态为fulfilled,执行onFulfilled,传入成功的值
if (this.status === 'resolved') {
onFulfilled(this.value);
};
// 状态为rejected,执行onRejected,传入失败的原因
if (this.status === 'rejected') {
onRejected(this.reason);
};
}
var p = new myPromise(function (resolve, reject) { resolve(1) });
console.log(p) //结果是返回一个构造函数,myPromise {status: "resolved", value: 1, reason: undefined}reason: undefinedstatus: "resolved"value: 1__proto__: Object
p.then(function (x) { console.log(x) })
比如setTimeout里resolve,这里需要用2个数组onFullfilledArray和onRejectedArray来保存异步的方法。在状态发生改变时,一次性遍历执行数组中的方法。
// 定义三种状态、resolve和reject方法、then方法(onFulfilled、onRejected)
// 支持异步resolve.
function myPromise(constructor) {
this.status = "pending" //定义初始状态
this.value = undefined;//存储成功后的值
this.reason = undefined;//存储失败的原因
// 成功存放的数组
this.onResolvedCallbacks = [];
// 失败存放法数组
this.onRejectedCallbacks = [];
let resolve = value => {
if (this.status === "pending") {
console.log(1)
this.status = "resolved";
this.value = value;
// 一旦resolve执行,调用成功数组的函数
this.onResolvedCallbacks.forEach(fn => fn());
console.log(2)
}
}
let reject = reason => {
if (this.status === "pending") {
this.status = "rejected";
this.reason = reason;
// 一旦reject执行,调用失败数组的函数
this.onRejectedCallbacks.forEach(fn => fn());
}
}
//捕获构造异常
try {
constructor(resolve, reject);
} catch (e) {
reject(e);
}
}
myPromise.prototype.then = function (onFulfilled, onRejected) {
// 状态为fulfilled,执行onFulfilled,传入成功的值
if (this.status === 'resolved') {
onFulfilled(this.value);
};
// 状态为rejected,执行onRejected,传入失败的原因
if (this.status === 'rejected') {
onRejected(this.reason);
};
// 当状态status为pending时
if (this.status === 'pending') {
// onFulfilled传入到成功数组
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value);
})
// onRejected传入到失败数组
this.onRejectedCallbacks.push(() => {
onRejected(this.reason);
})
}
}
// ------------------------------------------
// 如果按版本1,那么主线程上的同步任务也就是3和4是已经执行完了,最后只是执行了resolve方法,then方法未执行
// 所以用两个数组来临时存储下异步的方法,刚开始也是直接执行then,但是这次全给添加到数组里去了,当定时器时间到了执行resolve方法时,直接执行数组里的方法即可
var p2 = new myPromise(function (resolve, reject) { setTimeout(function () { resolve(22) }, 2000) });
p2.then(function (x) { console.log("x",x) }) //22
console.log("p2", p2)
let myCanvas = document.getElementById("myCanvas");
myCanvas.width = "500";
myCanvas.height = "300";
var ctx = myCanvas.getContext("2d");
//画三角形
ctx.moveTo(50, 50);
ctx.lineTo(250, 100);
ctx.lineTo(50, 200);
ctx.lineTo(50, 50);
//画直线
ctx.moveTo(350, 50);
ctx.lineTo(350, 200);
//定义画线样式
ctx.strokeStyle = "red";
ctx.lineWidth = "5";
ctx.lineCap = "round"; // 线条两端的形状
ctx.lineJoin = "miter" // 线条交界处形状
ctx.stroke();
ctx.beginPath(); // 第一个状态的beginPath可省略
ctx.moveTo(50, 50); // 有beginPath可改成lineTo()
ctx.lineTo(250, 100);
ctx.lineTo(50, 200);
ctx.lineTo(50, 50); // 用了closePath可省此句
ctx.closePath() // 绘制封闭图形时closePath可避免粗线条时显示问题
// ctx.fillStyle = "blue";
// ctx.fill(); // 填充
ctx.strokeStyle = "red";
ctx.stroke();
ctx.beginPath();
ctx.moveTo(350, 50);
ctx.lineTo(350, 200);
ctx.strokeStyle = "yellow";
ctx.stroke();
let image = new Image();
image.src = "img/img.png";
image.onload = function() {
context.drawImage(image, 0, 0);
}
function drawImageByScale(scale) {
let imageWidth = 1152 * scale
let imageHeight = 768 * scale
// 计算缩放后图片的起始点,如果是放大的,那么<0,如果小到溢出画布的部分不会被显示
x = canvas.width / 2 - imageWidth / 2
y = canvas.height / 2 - imageHeight / 2
// 先清除画布,然后进行绘制缩放后的图片
context.clearRect(0, 0, canvas.width, canvas.height)
context.drawImage(image, x, y, imageWidth, imageHeight)
}
将第二个canvas加载到第一个canvas,第二个通常不可见
function drawImage(image, scale) {
imageWidth = 1152 * scale
imageHeight = 768 * scale
x = canvas.width / 2 - imageWidth / 2
y = canvas.height / 2 - imageHeight / 2
context.clearRect(0, 0, canvas.width, canvas.height)
// 绘制原图
context.drawImage(image, x, y, imageWidth, imageHeight)
// 绘制水印
context.drawImage(watermarkCanvas, canvas.width - watermarkCanvas.width,
canvas.height - watermarkCanvas.height)
}
<canvas id="canvas" style="display:block;margin:0 auto;border:1px solid #aaa;">
您的浏览器尚不支持canvas
</canvas>
<canvas id="offCanvas" style="display: none"></canvas>
// 主canvas
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
// 离屏canvas
let offCanvas = document.getElementById("offCanvas")
let offContext = offCanvas.getContext("2d")
let image = new Image()
let isMouseDown = false
let scale
window.onload = function () {
canvas.width = 1152
canvas.height = 768
image.src = "img-lg.jpg"
image.onload = function () {
offCanvas.width = image.width
offCanvas.height = image.height
scale = offCanvas.width / canvas.width
context.drawImage(image, 0, 0, canvas.width, canvas.height)
offContext.drawImage(image, 0, 0)
}
}
// 返回鼠标相对于canvas左上角的坐标
function windowToCanvas(x, y) {
let bbox = canvas.getBoundingClientRect()
return { x: x - bbox.left, y: y - bbox.top }
}
// 鼠标按下
canvas.onmousedown = function (e) {
e.preventDefault()
isMouseDown = true
point = windowToCanvas(e.clientX, e.clientY)
drawCanvasWithMagnifier(true, point)
}
// 鼠标松开
canvas.onmouseup = function (e) {
e.preventDefault()
isMouseDown = false
drawCanvasWithMagnifier(false)
}
// 鼠标移出canvas区域
canvas.onmouseout = function (e) {
e.preventDefault()
isMouseDown = false
drawCanvasWithMagnifier(false)
}
// 鼠标在canvas区域内移动
canvas.onmousemove = function (e) {
e.preventDefault()
if (isMouseDown == true) { // 即为按下同时滑动
point = windowToCanvas(e.clientX, e.clientY)
drawCanvasWithMagnifier(true, point)
}
}
// 在离屏canvas上绘制放大镜
function drawCanvasWithMagnifier(isShowMagnifier, point) {
// 先在主canvas上清除一下,然后绘制原图
context.clearRect(0, 0, canvas.width, canvas.height)
context.drawImage(image, 0, 0, canvas.width, canvas.height)
if (isShowMagnifier === true) {
drawMagnifier(point)
}
}
function drawMagnifier(point) {
let mr = 200 // 放大镜半径
let imageLG_cx = point.x * scale
let imageLG_cy = point.y * scale
// 缩放后的x、y值减去半径,得到离屏canvas的开始坐标
let sx = imageLG_cx - mr
let sy = imageLG_cy - mr
// canvas的开始坐标
let dx = point.x - mr
let dy = point.y - mr
context.save() // 保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作
context.lineWidth = 10.0
context.strokeStyle = "#069"
context.beginPath()
// 点击的当前坐标为圆心绘制圆形放大镜
context.arc(point.x, point.y, mr, 0, Math.PI * 2, false)
context.stroke()
context.clip()// 裁剪区域,设定裁剪区域后,只有在区域内的图像才能显示,其余部分会被屏蔽掉
// 将offCanvas上起点为(sx,sy)的区域----渲染到---->canvas上起点为(dx,dy)的区域
// 至于*2,是因为mr只是半径
context.drawImage(offCanvas, sx, sy, 2 * mr, 2 * mr, dx, dy, 2 * mr, 2 * mr)
context.restore() // 用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响
}
function greyEffect() {
var imageData = contexta.getImageData(0, 0, canvasa.width, canvasa.height)
var pixelData = imageData.data
for (var i = 0; i < canvasb.width * canvasb.height; i++) {
var r = pixelData[i * 4 + 0]
var g = pixelData[i * 4 + 1]
var b = pixelData[i * 4 + 2]
var grey = r * 0.3 + g * 0.59 + b * 0.11
pixelData[i * 4 + 0] = grey
pixelData[i * 4 + 1] = grey
pixelData[i * 4 + 2] = grey
}
contextb.putImageData(imageData, 0, 0, 0, 0, canvasb.width, canvasb.height)
context.putImageData()
}
console.log(1)
setTimeout(function() {
//settimeout1
console.log(2)
}, 0);
const intervalId = setInterval(function() {
//setinterval1
console.log(3)
}, 0)
setTimeout(function() {
//settimeout2
console.log(10)
new Promise(function(resolve) {
//promise1
console.log(11)
resolve()
})
.then(function() {
console.log(12)
})
.then(function() {
console.log(13)
clearInterval(intervalId)
})
}, 0);
//promise2
Promise.resolve()
.then(function() {
console.log(7)
})
.then(function() {
console.log(8)
})
console.log(9)
1 - 9 - 7 - 8 - 2 - 3 - 10 - 11 - 12 -13
第一次事件循环:
第二次事件循环:
第三次事件循环:
第四次事件循环:
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
还原该语法糖后
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(()=>console.log('async1 end'))
}
async function async2() {
console.log('async2');
}
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
结果:
/**
* async1 start
* async2
* promise1
* script end
* async1 end (上一个tip被加入microTask)
* promise2 (上一个tip被加入microTask)
* */
await会产生一个微任务(Promise.then是微任务)
Action发出后,Reducer立刻计算出State,这是同步。
但如果需要在Action发出后(即store.dispatch())过一段时间再执行Reducer(即进入Store),这就是异步了。此时就需要用到中间件middleware
中间件就是一个函数,对store.dispatch方法进行了改造,在 发出 Action 和 执行 Reducer 这两步之间,添加了其他功能。
如果有一个ajax异步请求
componentDidMount() {
axios.get('/list.json').then((res)=>{
const data = res.data;
const action = initListAction(data); // initListAction来自actionCreators
store.dispatch(action)
})
}
但是不希望在组件内写,需要统一把复杂请求或异步请求放到action里去处理,那么就用到了redux-thunk中间件,重写的代码如下
componentDidMount(e) {
const action = getTodoList(); // 返回的是函数
store.dispatch(action); // 由于有redux-thunk,会执行这个函数
}
//actionCreater.js
export const getTodoList = () => {
return (dispatch) => {
axios.get('/list.json').then((res) => {
const data = res.data;
const action = initListAction(data);
dispatch(action); // 也就是前面说的发出action后,到现在才执行Reducer,因为现在Action是对象了
})
}
}
1、监听器Observer,通过Proxy或Object.defineProperty来劫持并监听所有属性,如果有变动的,就通知订阅者
2、解析器Compile,对每个元素节点的指令进行扫描和解析,页面初始化时根据指令模板替换数据,以及绑定相应的更新函数
3、Dep,收集多个订阅者,它是在监听器Observer和订阅者Watcher之间进行统一管理的
4、订阅者Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图(在Compile里初始化)
① 当读取所需对象的值时,会触发依赖收集,目的是将相关订阅者也就是Watcher,存放到的订阅器Dep(存放Watcher观察者对象)的subs中,形成的关系如下。(触发的方式是初始化Watcher的时候默认触发了getter)
② 在修改对象的值的时候,会通知之前依赖收集得到的订阅器Dep中的每一个Watcher,这些watcher都依赖着这个值,然后会告诉他们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图,当然这中间还有一个 patch和re-render 的过程 (setter-->watcher-update)
(有多个订阅者Watcher,watcher在初始化的时候会将自己装进订阅器Dep中)
Q1:Watcher,Dep,Observer这几个类之间的关系?
A1:Watcher是观察者观察经过Observer封装过的数据,Dep是Watcher和观察数据间的纽带,主要起到依赖收集和通知更新的作用。
Q2:Dep中的subs存储的是什么?
A2: subs存储的是观察者Watcher实例。
Q3:Dep.target是什么,该值是何处赋值的?
A4:Dep是一个类,Dep.target是类的属性,并不是dep实例的属性,在全局可访问到,所以Dep.target在全局能访问到,可以任意改变它的值,保存当前的watcher实例,在new Watcher()的时候进行赋值,赋值为当前Watcher实例,订阅完成后设置为null(get这个方法使用很平常,不可能每次使用获取数据值的时候都去触发依赖收集)
实现mvvm主要包含两个方面
1、视图变化更新数据
事件监听,如input输入框的change、keyup事件
2、数据变化更新视图
重点是如何知道数据变了,就是通过Object.defineProperty( )对属性设置一个set函数,当数据改变了就会来触发这个函数,所以我们只要将一些需要更新的方法放在这里面就可以实现data更新view了。
JavaScript引擎比起这些步骤要复杂,比如在语法分析和代码生成阶段有特定的步骤来对性能进行优化,包括对冗余代码进行优化
很多流行的语言都可以通过AST解析成一颗语法树,也可以认为是一个JSON,比如:CSS、HTML、JavaScript、PHP、Java、SQL等
1、JavaScript
2、HTML
3、CSS
这里用到的解析器是babylon、
import * as babylon from "babylon"; // js的词法解析器
import traverse from "babel-traverse"; // 遍历ast
import generate from "babel-generator"; // 根据ast生成代码
const code = `const a = 5;`
// 解析生成ast
const ast = babylon.parse(code);
// 遍历ast做修改
traverse(ast,{
enter(path){
if(path.node.type === 'VariableDeclaration' ){
path.node.kind = 'var'
}
}
})
// 生成代码
let res = generate(ast,{},code).code
console.log(res)
自动分号插入 (automatic semicolon insertion, ASI) ,在JS的语法分析 (parsing)阶段起作用,大部分语句如声明、表达式、return、break等都需要;,但块语句、if、try等语句是不需要;的
规则都基于以下两点:
比如,新行并入当前行将构成非法语句,自动插入分号,在continue,return,break,throw后自动插入分号,++、--后缀表达式作为新行的开始,在行首自动插入分号
<body>
<div class='wrapper'>
<button class='like-btn'>
<span class='like-text'>点赞</span>
<span>👍</span>
</button>
</div>
</body>
// --------------------------------------------------
const button = document.querySelector('.like-btn')
const buttonText = button.querySelector('.like-text')
let isLiked = false
button.addEventListener('click', () => {
isLiked = !isLiked
if (isLiked) {
buttonText.innerHTML = '取消'
} else {
buttonText.innerHTML = '点赞'
}
}, false)
class LikeButton {
render() {
return `
<button id='like-btn'>
<span class='like-text'>赞</span>
<span>👍</span>
</button>
`
}
}
// ----------------------------------------
const wrapper = document.querySelector('.wrapper')
const likeButton1 = new LikeButton()
wrapper.innerHTML = likeButton1.render()
const likeButton2 = new LikeButton()
wrapper.innerHTML += likeButton2.render()
const createDOMFromString = (domString) => {
const div = document.createElement('div')
div.innerHTML = domString
return div
}
class LikeButton {
constructor() {
this.state = { isLiked: false }
}
setState(state) {
const oldEl = this.el
this.state = state
this.el = this.render()
this.onStateChange && this.onStateChange(oldEl, this.el) // 实例化后被创建
}
changeLikeText() {
this.setState({
isLiked: !this.state.isLiked
})
}
render() {
this.el = createDOMFromString(`
<button class='like-btn'>
<span class='like-text'>${this.state.isLiked ? '取消' : '点赞'}</span>
<span>👍</span>
</button>
`)
// 如果不通过bind,那么真正执行的时候changeLikeText里面的this是window
// 因为new的时候是直接执行changeLikeText
this.el.addEventListener('click', this.changeLikeText.bind(this), false)
return this.el
}
}
// ------------------------------------------------------------------------------------
const likeButton = new LikeButton()
wrapper.appendChild(likeButton.render()) // 第一次插入 DOM 元素
likeButton.onStateChange = (oldEl, newEl) => {
wrapper.insertBefore(newEl, oldEl) // 插入新的元素
wrapper.removeChild(oldEl) // 删除旧的元素
}
优化DOM操作,一旦状态发生改变,就重新调用 render 方法,构建一个新的 DOM 元素。
具体来说当用户点击时,会调用setState,setState在改变状态的同时内部会调用render,render会根据state的不同构建不同的DOM元素(数据驱动),最后会通过 onStateChange 告知外部插入新的 DOM 元素,然后删除旧的元素,页面就更新了。
const createDOMFromString = (domString) => {
const div = document.createElement('div')
div.innerHTML = domString
return div
}
class LikeButton {
constructor() {
this.state = { isLiked: false }
}
setState(state) {
const oldEl = this.el // 旧dom
this.state = state
this.el = this.render() // 最新的dom
this.onStateChange && this.onStateChange(oldEl, this.el) // 实例化后被创建
}
changeLikeText() {
this.setState({
isLiked: !this.state.isLiked
})
}
render() {
this.el = createDOMFromString(`
<button class='like-btn'>
<span class='like-text'>${this.state.isLiked ? '取消' : '点赞'}</span>
<span>👍</span>
</button>
`)
this.el.addEventListener('click', this.changeLikeText.bind(this), false)
return this.el
}
}
// ------------------------------------------------------------------------------------
const likeButton = new LikeButton()
wrapper.appendChild(likeButton.render()) // 第一次插入 DOM 元素
likeButton.onStateChange = (oldEl, newEl) => {
wrapper.insertBefore(newEl, oldEl) // 插入新的元素
wrapper.removeChild(oldEl) // 删除旧的元素
}
为了能够写更多的类似组件,可以把这种模式抽象出来,放到一个Component中,让所有组件都能继承这个父类。子类继承后只需要实现一个返回HTML字符串的render方法
还有一个额外的 mount 的方法,其实就是把组件的 DOM 元素插入页面,并且在 setState 的时候更新页面:
class Component {
constructor(props = {}) {
this.props = props
}
setState(state) {
const oldEl = this.el
this.state = state
this.el = this.renderDOM()
if (this.onStateChange) this.onStateChange(oldEl, this.el)
}
renderDOM() {
this.el = createDOMFromString(this.render())
if (this.onClick) {
this.el.addEventListener('click', this.onClick.bind(this), false)
}
return this.el
}
}
const createDOMFromString = (domString) => {
const div = document.createElement('div')
div.innerHTML = domString
return div
}
const mount = (component, wrapper) => {
wrapper.appendChild(component.renderDOM())
component.onStateChange = (oldEl, newEl) => {
wrapper.insertBefore(newEl, oldEl)
wrapper.removeChild(oldEl)
}
}
之后再需要其他组件的时候,就能直接继承了
class LikeButton extends Component {
// 把 props 传给父类,相当于父类执行一遍构造函数,这样就可以通过 this.props获取到配置参数:
constructor (props) {
super(props)
this.state = { isLiked: false }
}
onClick () {
this.setState({
isLiked: !this.state.isLiked
})
}
render () {
return `
<button class='like-btn' style=`background-color: ${this.props.bgColor}`>
<span class='like-text'>
${this.state.isLiked ? '取消' : '点赞'}
</span>
<span>👍</span>
</button>
`
}
}
mount(new LikeButton({ bgColor: 'red' }), wrapper)
连续触发事件过程中有一定的时间间隔。
比如人的眨眼睛,就是一定时间内眨一次。
function throttle(func, delay) {
var prev = Date.now();
return function () {
var context = this;
var args = arguments;
var now = Date.now();
if (now - prev >= delay) {
func.apply(context, args);
prev = Date.now();
}
}
}
短时间内多次触发同一事件只执行最后一次
如果有人陆续刷卡上车,司机就不会开车。只有别人没刷卡了,司机才开车。一定要停下来才能开始计时
function debounce(func, delay, immediate) {
var timer = null;
return function () {
var context = this;
var args = arguments;
if (timer) clearTimeout(timer);
// 立即执行
if (immediate) {
var doNow = !timer;
//一开始的时候还没设置定时器,timer还是null,之后就是等于调用次数了
// 所有第一次doNow是true,之后设置一个定时器,delay之后重置为null,
// 在此之前,都不为null,因为已经设置了
if (doNow) {
func.apply(context, args);
}
timer = setTimeout(function () {
console.log("timergg", timer)
timer = null;
}, delay);
// 不立即执行
} else {
timer = setTimeout(function () {
func.apply(context, args);
}, delay);
}
}
}
布局和绘制涉及到两个名词: reflow(重排/回流)和repain(重绘)。页面在首次加载时必然会经历reflow和repain
图一也叫做像素管道,这里就不多做阐述了
举个栗子
<p class="a">abc</p>
是CSSOM,造成CSSOM影响DOM构建的原因还是由于script标签的中途插入,当JS想修改样式时,必须拿到完整的CSSOM,如果浏览器尚未完成CSSOM的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟JS执行和DOM的构建,直至完成CSSOM的下载和构建
render tree只包含了用于渲染页面的节点
为了形成渲染树,浏览器大致做的事情如下:
浏览器是多进程的,浏览器的渲染进程是多线程的;
上面提到了composite,浏览器渲染的图层大致分为2种:普通图层、复合图层
独立于普通文本流中,改动可以避免整个页面的reflow和repaint,提升性能,但是也要注意不要过度使用,否则资源也会消耗过度,适得其反
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.